diff --git a/debian/13-trixie-luks/debian-trixie.pkr.hcl b/debian/13-trixie-luks/debian-trixie.pkr.hcl index 5540149..1c60656 100644 --- a/debian/13-trixie-luks/debian-trixie.pkr.hcl +++ b/debian/13-trixie-luks/debian-trixie.pkr.hcl @@ -20,7 +20,7 @@ source "proxmox-iso" "debian-13-trixie-luks" { node = "${var.proxmox_node}" vm_id = "${var.template_vm_id}" vm_name = "debian-13-trixie-luks-${local.timestamp}" - template_description = "Debian 13 Trixie, LUKS encrypted, built with Packer on ${local.timestamp}" + template_description = "Debian 13 Trixie, LUKS encrypted, built with Packer on ${local.timestamp}\n\nLUKS default passphrase: `${var.default_luks_passphrase}`" os = "l26" qemu_agent = true @@ -45,6 +45,10 @@ source "proxmox-iso" "debian-13-trixie-luks" { efi_type = "4m" } + serials = [ + "socket" + ] + # Download ISO boot_iso { type = "scsi" @@ -75,6 +79,7 @@ source "proxmox-iso" "debian-13-trixie-luks" { "c", "linux /install.amd/vmlinuz auto-install/enable=true priority=critical ", "DEBIAN_FRONTEND=text ", + "console=tty0 console=ttyS0,115200 earlyprintk=ttyS0,115200 consoleblank=0 ", "passwd/root-password='${var.default_root_passphrase}' ", "passwd/root-password-again='${var.default_root_passphrase}' ", "partman-crypto/passphrase='${var.default_luks_passphrase}' ", @@ -101,21 +106,38 @@ build { name = "debian-13-trixie-luks-image" sources = ["source.proxmox-iso.debian-13-trixie-luks"] - # Provisioning the VM Template for Cloud-Init Integration in Proxmox #1 + # Install dependencies and default packages provisioner "shell" { inline = [ - "rm /etc/ssh/ssh_host_*", - "truncate -s 0 /etc/machine-id", - "apt -y autoremove --purge 2> /dev/null", - "apt -y clean 2> /dev/null", - "apt -y autoclean 2> /dev/null", - "rm -rf /var/cache/apt/archives /var/lib/apt/lists/*", - "cloud-init clean", - "rm -f /etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg", - "sync" + "export DEBIAN_FRONTEND=noninteractive", + "apt-get update", + "apt-get install -y age apt-transport-https aria2 bat bc bmon btop ca-certificates curl duf eza fastfetch fzf git gnupg htop iftop iotop iperf jq lsof magic-wormhole mosh mtr ncdu parted progress pv ripgrep rsync smartmontools socat sudo tmux usbutils vim wget yq zsh zstd" ] } + # Install Tailscale + provisioner "shell" { + script = "debian/13-trixie-luks/scripts/tailscale.sh" + } + + # Setup CrowdSec Repo + provisioner "shell" { + script = "debian/13-trixie-luks/scripts/crowdsec-repo-setup.sh" + } + + # Install CrowdSec + provisioner "shell" { + inline = [ + "apt-get install -y crowdsec", + "apt-get install -y crowdsec-firewall-bouncer-iptables" + ] + } + + # Configure CrowdSec + provisioner "shell" { + script = "debian/13-trixie-luks/scripts/crowdsec-configuration.sh" + } + # Provisioning the VM Template for Cloud-Init Integration in Proxmox #2 provisioner "file" { source = "debian/13-trixie-luks/files/99-pve.cfg" @@ -137,4 +159,62 @@ build { source = "debian/13-trixie-luks/files/debian.sources" destination = "/etc/apt/sources.list.d/debian.sources" } + + provisioner "file" { + source = "debian/13-trixie-luks/files/90-initial-login-setup.sh" + destination = "/etc/profile.d/90-initial-login-setup.sh" + } + + provisioner "file" { + source = "debian/13-trixie-luks/files/initial-setup.sh" + destination = "/usr/local/bin/initial-setup.sh" + } + + provisioner "shell" { + inline = [ + "chmod +x /usr/local/bin/initial-setup.sh" + ] + } + + # Install Clevis + provisioner "shell" { + inline = [ + "apt-get update", + "apt-get install -y clevis clevis-luks clevis-initramfs" + ] + } + + # Setup Serial Console for xterm.js in Proxmox VE + provisioner "shell" { + inline = [ + "sed -i 's/#\\?GRUB_CMDLINE_LINUX=.*\"/GRUB_CMDLINE_LINUX=\"console=tty0 console=ttyS0,115200 earlyprintk=ttyS0,115200 consoleblank=0\"/' /etc/default/grub", + "sed -i 's/#\\?GRUB_TERMINAL=.*/GRUB_TERMINAL=\"serial console\"/' /etc/default/grub", + "sed -i 's/#\\?GRUB_SERIAL_COMMAND=.*/GRUB_SERIAL_COMMAND=\"serial --speed=115200\"/' /etc/default/grub", + "update-grub" + ] + } + + # Provisioning the VM Template for Cloud-Init Integration in Proxmox #1 + provisioner "shell" { + inline = [ + "rm /etc/ssh/ssh_host_*", + "truncate -s 0 /etc/machine-id", + "apt -y autoremove --purge 2> /dev/null", + "apt -y clean 2> /dev/null", + "apt -y autoclean 2> /dev/null", + "rm -rf /var/cache/apt/archives /var/lib/apt/lists/*", + "cloud-init clean", + "rm -f /etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg", + "sync" + ] + } + + # Remove temporary settings and configuration for packer build + provisioner "shell" { + inline = [ + "sed -i 's/^#\\?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config", + "sed -i 's/^#\\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config", + "passwd -dl root" + ] + } } diff --git a/debian/13-trixie-luks/files/90-initial-login-setup.sh b/debian/13-trixie-luks/files/90-initial-login-setup.sh new file mode 100644 index 0000000..2419845 --- /dev/null +++ b/debian/13-trixie-luks/files/90-initial-login-setup.sh @@ -0,0 +1,26 @@ +#! /bin/sed 2,5!d;s/^#.// +# This script must be sourced from within a shell +# and not executed. For instance with: +# +# . /usr/local/bin/initial-setup.sh + +# Only run in interactive shells +case $- in + *i*) ;; + *) return ;; +esac + +if [ "$EUID" -ne 0 ]; then + if ! command -v sudo >/dev/null 2>&1 || ! sudo -n true >/dev/null 2>&1; then + echo "Error: must be root or have sudo privileges to run initial login setup." >&2 + return + fi +fi + +SENTINEL="/var/lib/initial-login-setup.done" + +if [ ! -f "$SENTINEL" ] && [ -x /usr/local/bin/initial-setup.sh ]; then + #DEBUG touch SENTINEL before running the setup script to prevent infinite loops during development + sudo /usr/local/bin/initial-setup.sh + sudo touch "$SENTINEL" +fi diff --git a/debian/13-trixie-luks/files/initial-setup.sh b/debian/13-trixie-luks/files/initial-setup.sh new file mode 100644 index 0000000..bc7fc2f --- /dev/null +++ b/debian/13-trixie-luks/files/initial-setup.sh @@ -0,0 +1,740 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +SCRIPT_NAME="$(basename "$0")" +TASK_INDEX=0 +TASK_TOTAL=0 +TEMP_FILES=() +STORAGE_DETECTED=0 + +ROOT_SOURCE="" +VG_NAME="" +LV_NAME="" +PV_NAME="" +LUKS_DEV="" +LUKS_NAME="" +LUKS_PART="" +DISK_DEV="" +PART_NUM="" + +if [ -t 1 ]; then + BOLD="$(tput bold 2>/dev/null || true)" + DIM="$(tput dim 2>/dev/null || true)" + RED="$(tput setaf 1 2>/dev/null || true)" + GREEN="$(tput setaf 2 2>/dev/null || true)" + YELLOW="$(tput setaf 3 2>/dev/null || true)" + BLUE="$(tput setaf 4 2>/dev/null || true)" + RESET="$(tput sgr0 2>/dev/null || true)" +else + BOLD="" + DIM="" + RED="" + GREEN="" + YELLOW="" + BLUE="" + RESET="" +fi + +die() { + echo "${RED}Error:${RESET} $*" >&2 + exit 1 +} + +log_info() { + echo "${BLUE}INFO:${RESET} $*" +} + +log_ok() { + echo "${GREEN}OK:${RESET} $*" +} + +log_warn() { + echo "${YELLOW}WARN:${RESET} $*" +} + +section() { + local title="$1" + echo + echo "${BOLD}${title}${RESET}" + echo "${DIM}------------------------------------------------------------${RESET}" +} + +add_temp_file() { + TEMP_FILES+=("$1") +} + +cleanup() { + local file + for file in "${TEMP_FILES[@]:-}"; do + [ -f "$file" ] && rm -f "$file" + done +} +trap cleanup EXIT + +ensure_tty() { + if [ ! -t 0 ] || [ ! -t 1 ]; then + die "This setup must run interactively in a TTY." + fi +} + +ensure_root() { + if [ "${EUID:-$(id -u)}" -ne 0 ]; then + if command -v sudo >/dev/null 2>&1; then + log_info "Re-running with sudo..." + exec sudo -E "$0" "$@" + fi + die "Must be root or have sudo privileges to run this setup." + fi +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + log_warn "Missing command: $cmd" + return 1 + fi + return 0 +} + +prompt_input() { + local label="$1" + local default="${2:-}" + local value="" + if [ -n "$default" ]; then + read -r -p "${label} [${default}]: " value /dev/tty + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + log_warn "Value cannot be empty." + done +} + +prompt_secret_confirm() { + local label="$1" + local confirm_label="$2" + local a="" + local b="" + while true; do + read -r -s -p "${label}: " a /dev/tty + read -r -s -p "${confirm_label}: " b /dev/tty + if [ -z "$a" ]; then + log_warn "Value cannot be empty." + continue + fi + if [ "$a" != "$b" ]; then + log_warn "Values do not match. Please try again." + continue + fi + printf '%s' "$a" + return 0 + done +} + +confirm() { + local label="$1" + local default="${2:-yes}" + local prompt="" + local answer="" + + if [ "$default" = "yes" ]; then + prompt="[Y/n]" + else + prompt="[y/N]" + fi + + while true; do + read -r -p "${label} ${prompt} " answer /dev/null 2>&1; then + numfmt --to=iec --suffix=B "$bytes" + else + awk -v b="$bytes" 'BEGIN { + split("B KiB MiB GiB TiB", u, " "); + i=1; + while (b>=1024 && i<5) { b/=1024; i++; } + printf "%.1f %s", b, u[i]; + }' + fi +} + +mib_to_human() { + local mib="$1" + awk -v m="$mib" 'BEGIN { printf "%.1f GiB", m/1024 }' +} + +add_task() { + TASK_TITLES+=("$1") + TASK_FUNCS+=("$2") + TASK_TOTAL=$((TASK_TOTAL + 1)) +} + +run_tasks() { + local i + local title + local func + for i in "${!TASK_FUNCS[@]}"; do + TASK_INDEX=$((TASK_INDEX + 1)) + title="${TASK_TITLES[$i]}" + func="${TASK_FUNCS[$i]}" + section "Task ${TASK_INDEX}/${TASK_TOTAL}: ${title}" + "$func" + done +} + +lsblk_attr() { + local path="$1" + local attr="$2" + lsblk -dn -o "$attr" "$path" 2>/dev/null | head -n1 | xargs || true +} + +infer_disk_part_from_partition() { + local part_path="$1" + local resolved + local base + + resolved="$(readlink -f "$part_path" 2>/dev/null || printf '%s' "$part_path")" + base="$(basename "$resolved")" + + if [[ "$base" =~ ^(nvme[0-9]+n[0-9]+)p([0-9]+)$ ]]; then + printf '/dev/%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + return 0 + fi + if [[ "$base" =~ ^(mmcblk[0-9]+)p([0-9]+)$ ]]; then + printf '/dev/%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + return 0 + fi + if [[ "$base" =~ ^(md[0-9]+)p([0-9]+)$ ]]; then + printf '/dev/%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + return 0 + fi + if [[ "$base" =~ ^((sd|vd|xvd|hd)[a-z]+)([0-9]+)$ ]]; then + printf '/dev/%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]}" + return 0 + fi + return 1 +} + +resolve_luks_backing_partition() { + local mapper_path="$1" + local mapper_name="$2" + local mapper_name_alt="$3" + local part="" + + if command -v cryptsetup >/dev/null 2>&1; then + part="$(cryptsetup status "$mapper_name" 2>/dev/null | awk '/^[[:space:]]*device:/ {print $2; exit}' | xargs || true)" + if [ -z "$part" ] && [ -n "$mapper_name_alt" ] && [ "$mapper_name_alt" != "$mapper_name" ]; then + part="$(cryptsetup status "$mapper_name_alt" 2>/dev/null | awk '/^[[:space:]]*device:/ {print $2; exit}' | xargs || true)" + fi + if [ -z "$part" ]; then + part="$(cryptsetup status "$mapper_path" 2>/dev/null | awk '/^[[:space:]]*device:/ {print $2; exit}' | xargs || true)" + fi + fi + + if [ -z "$part" ]; then + part="$(lsblk -nro PATH,TYPE -s "$mapper_path" 2>/dev/null | awk '$2=="part" {print $1; exit}' | xargs || true)" + fi + + printf '%s' "$part" +} + +refresh_partition_table() { + local disk="$1" + if command -v partprobe >/dev/null 2>&1; then + partprobe "$disk" || true + fi + if command -v partx >/dev/null 2>&1; then + partx -u "$disk" || true + fi + if command -v udevadm >/dev/null 2>&1; then + udevadm settle || true + fi +} + +wait_for_partition_growth() { + local part="$1" + local old_bytes="$2" + local new_bytes=0 + local i=0 + + for ((i = 0; i < 12; i++)); do + new_bytes="$(blockdev --getsize64 "$part" 2>/dev/null || echo 0)" + if [ "$new_bytes" -gt "$old_bytes" ]; then + return 0 + fi + sleep 1 + done + return 1 +} + +is_last_partition_on_disk() { + local disk="$1" + local part_num="$2" + local last_part + + last_part="$(parted -ms "$disk" unit s print 2>/dev/null | awk -F: '$1 ~ /^[0-9]+$/ {last=$1} END {print last}')" + [ -n "$last_part" ] && [ "$part_num" = "$last_part" ] +} + +get_trailing_free_bytes() { + local disk="$1" + local part_num="$2" + local disk_bytes + local part_end_bytes + local free_bytes + + disk_bytes="$(blockdev --getsize64 "$disk" 2>/dev/null || true)" + part_end_bytes="$(parted -ms "$disk" unit B print 2>/dev/null | awk -F: -v p="$part_num" '$1==p {gsub("B","",$3); print $3; exit}')" + if [ -z "$disk_bytes" ] || [ -z "$part_end_bytes" ]; then + return 1 + fi + + free_bytes=$((disk_bytes - part_end_bytes - 1)) + if [ "$free_bytes" -lt 0 ]; then + free_bytes=0 + fi + echo "$free_bytes" +} + +resize_open_luks_mapping() { + if [ -n "$LUKS_NAME" ] && cryptsetup resize "$LUKS_NAME" >/dev/null 2>&1; then + return 0 + fi + cryptsetup resize "$LUKS_DEV" >/dev/null 2>&1 +} + +detect_storage_stack() { + STORAGE_DETECTED=0 + LUKS_NAME="" + LUKS_PART="" + DISK_DEV="" + PART_NUM="" + + require_cmd findmnt || return 1 + require_cmd lvs || return 1 + require_cmd pvs || return 1 + require_cmd lsblk || return 1 + require_cmd cryptsetup || return 1 + + ROOT_SOURCE="$(findmnt -n -o SOURCE / || true)" + if [ -z "$ROOT_SOURCE" ] || [ ! -e "$ROOT_SOURCE" ]; then + log_warn "Unable to detect root device." + return 1 + fi + + VG_NAME="$(lvs --noheadings -o vg_name "$ROOT_SOURCE" 2>/dev/null | xargs || true)" + LV_NAME="$(lvs --noheadings -o lv_name "$ROOT_SOURCE" 2>/dev/null | xargs || true)" + if [ -z "$VG_NAME" ]; then + log_warn "Root does not appear to be on LVM." + return 1 + fi + + PV_NAME="$(pvs --noheadings -o pv_name --select "vg_name=${VG_NAME}" 2>/dev/null | head -n1 | xargs || true)" + if [ -z "$PV_NAME" ]; then + log_warn "Unable to detect LVM physical volume." + return 1 + fi + + LUKS_DEV="$PV_NAME" + LUKS_NAME="$(basename "$LUKS_DEV")" + local mapper_name_alt + mapper_name_alt="$(lsblk_attr "$LUKS_DEV" NAME)" + + local luks_type + luks_type="$(lsblk_attr "$LUKS_DEV" TYPE)" + if [ "$luks_type" != "crypt" ]; then + log_warn "LVM PV is not on a LUKS device (type: ${luks_type:-unknown}). LUKS resize will be skipped." + return 1 + fi + + LUKS_PART="$(resolve_luks_backing_partition "$LUKS_DEV" "$LUKS_NAME" "$mapper_name_alt")" + if [ -z "$LUKS_PART" ] || [ ! -b "$LUKS_PART" ]; then + log_warn "Unable to detect LUKS backing partition." + return 1 + fi + + local inferred + local inferred_disk="" + local inferred_part="" + inferred="$(infer_disk_part_from_partition "$LUKS_PART" || true)" + if [ -n "$inferred" ]; then + inferred_disk="$(printf '%s\n' "$inferred" | sed -n '1p')" + inferred_part="$(printf '%s\n' "$inferred" | sed -n '2p')" + fi + + DISK_DEV="$inferred_disk" + PART_NUM="$inferred_part" + + if [ -z "$DISK_DEV" ]; then + local disk_parent + disk_parent="$(lsblk_attr "$LUKS_PART" PKNAME)" + if [ -n "$disk_parent" ]; then + DISK_DEV="/dev/${disk_parent}" + else + DISK_DEV="$(lsblk -nro PATH,TYPE -s "$LUKS_PART" 2>/dev/null | awk '$2=="disk" {print $1; exit}' | xargs || true)" + fi + fi + + if [ -z "$PART_NUM" ]; then + PART_NUM="$(lsblk_attr "$LUKS_PART" PARTNUM)" + fi + + if [ -z "$DISK_DEV" ] || [ ! -b "$DISK_DEV" ] || [ -z "$PART_NUM" ] || ! [[ "$PART_NUM" =~ ^[0-9]+$ ]]; then + log_warn "Unable to detect disk device or partition number." + log_warn "Detected values: LUKS_PART=${LUKS_PART:-} DISK_DEV=${DISK_DEV:-} PART_NUM=${PART_NUM:-}" + return 1 + fi + + STORAGE_DETECTED=1 + return 0 +} + +resize_lvm_on_luks() { + if [ "$STORAGE_DETECTED" -ne 1 ]; then + log_warn "Storage layout not detected; skipping resize." + return 0 + fi + + require_cmd parted || return 0 + require_cmd blockdev || return 0 + require_cmd cryptsetup || return 0 + require_cmd pvresize || return 0 + require_cmd lvextend || return 0 + + log_info "Root LV: ${ROOT_SOURCE}" + log_info "VG: ${VG_NAME} | LV: ${LV_NAME}" + log_info "LUKS device: ${LUKS_DEV}" + log_info "LUKS partition: ${LUKS_PART}" + log_info "Disk: ${DISK_DEV} | Partition number: ${PART_NUM}" + [ -n "$LUKS_NAME" ] && log_info "LUKS mapper name: ${LUKS_NAME}" + + if ! is_last_partition_on_disk "$DISK_DEV" "$PART_NUM"; then + log_warn "Partition ${PART_NUM} is not the last partition on ${DISK_DEV}. Automatic growth is skipped." + return 0 + fi + + local part_size_before + local free_bytes + + part_size_before="$(blockdev --getsize64 "$LUKS_PART" 2>/dev/null || echo 0)" + free_bytes="$(get_trailing_free_bytes "$DISK_DEV" "$PART_NUM" || true)" + if [ -z "$free_bytes" ]; then + log_warn "Unable to determine free disk space." + return 0 + fi + + if [ "$free_bytes" -lt $((1024 * 1024)) ]; then + log_ok "No significant free space detected after the root partition." + return 0 + fi + + log_info "Unallocated space available: $(human_bytes "$free_bytes")" + if ! confirm "Extend partition, LUKS device, VG, and root LV now?" "yes"; then + log_warn "Skipped resize." + return 0 + fi + + log_info "Extending partition ${LUKS_PART} to 100% of disk..." + if ! parted -s "$DISK_DEV" resizepart "$PART_NUM" 100%; then + log_warn "Partition resize failed." + return 0 + fi + + refresh_partition_table "$DISK_DEV" + if [ "$part_size_before" -gt 0 ] && ! wait_for_partition_growth "$LUKS_PART" "$part_size_before"; then + log_warn "Kernel has not reported the new partition size yet; continuing anyway." + fi + + log_info "Resizing LUKS device..." + if ! resize_open_luks_mapping; then + log_warn "LUKS resize failed." + return 0 + fi + + log_info "Resizing LVM physical volume..." + if ! pvresize "$LUKS_DEV"; then + log_warn "pvresize failed." + return 0 + fi + + log_info "Extending root LV to use all free space..." + if ! lvextend -l +100%FREE -r "$ROOT_SOURCE"; then + log_warn "lvextend failed." + return 0 + fi + + log_ok "Resize complete." +} + +parse_size_to_mib() { + local input="$1" + local normalized + normalized="$(echo "$input" | tr '[:upper:]' '[:lower:]' | xargs)" + if [[ "$normalized" =~ ^[0-9]+$ ]]; then + echo "$normalized" + return 0 + fi + if [[ "$normalized" =~ ^([0-9]+)(g|gb)$ ]]; then + echo $((BASH_REMATCH[1] * 1024)) + return 0 + fi + if [[ "$normalized" =~ ^([0-9]+)(m|mb)$ ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + return 1 +} + +setup_swap() { + require_cmd awk || return 0 + require_cmd fallocate || return 0 + require_cmd mkswap || return 0 + require_cmd swapon || return 0 + + local mem_kib + local mem_mib + local swap_mib + + mem_kib="$(awk '/MemTotal/ {print $2}' /proc/meminfo)" + mem_mib=$((mem_kib / 1024)) + + if [ "$mem_mib" -lt 2048 ]; then + swap_mib=$((mem_mib * 2)) + elif [ "$mem_mib" -lt 4096 ]; then + swap_mib=$mem_mib + else + swap_mib=$((mem_mib / 5)) + fi + + log_info "Detected RAM: $(mib_to_human "$mem_mib")" + log_info "Recommended swap size: $(mib_to_human "$swap_mib")" + + if ! confirm "Use recommended swap size?" "yes"; then + local custom + while true; do + custom="$(prompt_input "Enter custom swap size (MiB or GiB, e.g. 2048 or 4G)")" + if swap_mib="$(parse_size_to_mib "$custom")"; then + if [ "$swap_mib" -gt 0 ]; then + break + fi + fi + log_warn "Invalid size. Try again." + done + fi + + local swapfile="/swapfile" + if [ -e "$swapfile" ]; then + log_warn "Swap file already exists at ${swapfile}." + if ! confirm "Replace existing swap file?" "no"; then + log_warn "Skipped swap setup." + return 0 + fi + if swapon --show=NAME --noheadings | grep -qx "$swapfile"; then + swapoff "$swapfile" || true + fi + rm -f "$swapfile" + fi + + log_info "Creating swap file (${swap_mib} MiB) at ${swapfile}..." + fallocate -l "${swap_mib}M" "$swapfile" + chmod 600 "$swapfile" + mkswap "$swapfile" >/dev/null + swapon "$swapfile" + + if ! grep -qE "^[[:space:]]*${swapfile}[[:space:]]" /etc/fstab; then + echo "${swapfile} none swap sw 0 0" >> /etc/fstab + fi + + log_ok "Swap enabled." +} + +change_luks_passphrase() { + if [ "$STORAGE_DETECTED" -ne 1 ]; then + log_warn "Storage layout not detected; skipping LUKS passphrase change." + return 0 + fi + if [ -z "$LUKS_PART" ] || [ ! -e "$LUKS_PART" ]; then + log_warn "LUKS partition not found; skipping." + return 0 + fi + require_cmd cryptsetup || return 0 + + if ! confirm "Change LUKS passphrase in slot 0 now?" "yes"; then + log_warn "Skipped LUKS passphrase change." + return 0 + fi + + local old_pass + local new_pass + local tmp_old + local tmp_new + + old_pass="$(prompt_secret "Enter current LUKS passphrase")" + new_pass="$(prompt_secret_confirm "Enter new LUKS passphrase" "Confirm new LUKS passphrase")" + + tmp_old="$(mktemp)" + tmp_new="$(mktemp)" + add_temp_file "$tmp_old" + add_temp_file "$tmp_new" + + printf '%s' "$old_pass" >"$tmp_old" + printf '%s' "$new_pass" >"$tmp_new" + + log_info "Updating LUKS passphrase in slot 0..." + if cryptsetup luksChangeKey --batch-mode --key-slot 0 --key-file "$tmp_old" "$LUKS_PART" "$tmp_new"; then + log_ok "LUKS passphrase updated." + else + log_warn "Failed to update LUKS passphrase." + fi +} + +setup_clevis() { + log_info "Clevis/Tang setup is not implemented in this template yet." + if confirm "Would you like to configure Clevis with a Tang server now? (will be skipped)" "no"; then + local tang + tang="$(prompt_input "Tang server URL" "http://tang.int.r3w.de")" + log_warn "Clevis setup for ${tang} is not implemented yet. Skipping." + else + log_info "Skipping Clevis setup." + fi +} + +setup_tailscale() { + if ! require_cmd tailscale; then + log_warn "Tailscale is not installed; skipping." + return 0 + fi + + if ! confirm "Set up Tailscale now?" "yes"; then + log_warn "Skipped Tailscale setup." + return 0 + fi + + local server + local tags + local key + local tag_list="" + local t + + server="$(prompt_input "Tailscale/Headscale server URL" "https://vpn.s1q.dev")" + tags="$(prompt_input "Client tags (comma-separated)" "server")" + key="$(prompt_secret "Pre-authentication key")" + + if [ -n "$tags" ]; then + IFS=',' read -r -a tag_array <<<"$tags" + for t in "${tag_array[@]:-}"; do + t="$(echo "$t" | xargs)" + if [ -n "$t" ]; then + if [ -n "$tag_list" ]; then + tag_list+="," + fi + tag_list+="tag:${t}" + fi + done + fi + + log_info "Bringing up Tailscale..." + if [ -n "$tag_list" ]; then + tailscale up --login-server "$server" --authkey "$key" --ssh --advertise-tags "$tag_list" + else + tailscale up --login-server "$server" --authkey "$key" --ssh + fi + + log_ok "Tailscale setup complete." +} + +setup_crowdsec() { + if ! require_cmd cscli; then + log_warn "CrowdSec (cscli) not installed; skipping." + return 0 + fi + + if ! confirm "Set up CrowdSec now?" "yes"; then + log_warn "Skipped CrowdSec setup." + return 0 + fi + + local key + key="$(prompt_secret "Enrollment key")" + + log_info "Enrolling CrowdSec..." + if cscli console enroll -e "$key"; then + log_info "Restarting CrowdSec service..." + systemctl restart crowdsec || log_warn "Failed to restart crowdsec service." + log_ok "CrowdSec enrollment complete." + else + log_warn "CrowdSec enrollment failed." + fi +} + +prompt_reboot() { + log_warn "A reboot is strongly recommended after partition or swap changes." + if confirm "Reboot now?" "yes"; then + log_info "Rebooting..." + reboot + else + log_info "Please reboot later to ensure changes take effect." + fi +} + +welcome() { + section "Initial VM Setup" + log_info "This setup runs once and is fully interactive." + log_info "Hostname: $(hostname)" +} + +main() { + ensure_tty + ensure_root "$@" + welcome + + if detect_storage_stack; then + log_ok "Detected LVM on LUKS storage layout." + else + log_warn "Storage layout detection incomplete; some steps may be skipped." + fi + + TASK_TITLES=() + TASK_FUNCS=() + + add_task "Resize LVM on LUKS (if free space exists)" resize_lvm_on_luks + add_task "Configure swap file" setup_swap + add_task "Change LUKS passphrase (slot 0)" change_luks_passphrase + add_task "Clevis/Tang setup (placeholder)" setup_clevis + add_task "Configure Tailscale" setup_tailscale + add_task "Configure CrowdSec" setup_crowdsec + add_task "Reboot recommendation" prompt_reboot + + run_tasks + log_ok "Initial setup finished." +} + +main "$@" diff --git a/debian/13-trixie-luks/http/meta-data b/debian/13-trixie-luks/http/meta-data deleted file mode 100644 index e69de29..0000000 diff --git a/debian/13-trixie-luks/http/preseed.cfg b/debian/13-trixie-luks/http/preseed.cfg index d139d75..b93751e 100644 --- a/debian/13-trixie-luks/http/preseed.cfg +++ b/debian/13-trixie-luks/http/preseed.cfg @@ -116,7 +116,7 @@ d-i partman-auto/expert_recipe string \ filesystem{ ext4 } \ mountpoint{ /boot } \ . \ - 25770 25770 25770 ext4 \ + 25770 25770 -1 ext4 \ $lvmok{ } \ lv_name{ root } \ method{ format } \ @@ -124,11 +124,6 @@ d-i partman-auto/expert_recipe string \ use_filesystem{ } \ filesystem{ ext4 } \ mountpoint{ / } \ - . \ - 1 10000 -1 ext4 \ - $lvmok{ } \ - lv_name{ reserved } \ - method{ keep } \ . d-i partman-partitioning/confirm_write_new_label boolean true @@ -140,7 +135,7 @@ d-i partman/confirm_nooverwrite boolean true d-i debconf/frontend select noninteractive tasksel tasksel/first multiselect standard, ssh-server -d-i pkgsel/include string qemu-guest-agent cloud-init curl vim +d-i pkgsel/include string cloud-init curl qemu-guest-agent sudo vim d-i pkgsel/upgrade select full-upgrade d-i pkgsel/update-policy select none d-i pkgsel/updatedb boolean true diff --git a/debian/13-trixie-luks/http/user-data b/debian/13-trixie-luks/http/user-data deleted file mode 100644 index 44d048d..0000000 --- a/debian/13-trixie-luks/http/user-data +++ /dev/null @@ -1,32 +0,0 @@ -#cloud-config -autoinstall: - version: 1 - locale: en_US - keyboard: - layout: us - ssh: - install-server: true - allow-pw: false - disable_root: true - ssh_quiet_keygen: true - allow_public_ssh_keys: true - apt: - preserve_sources_list: false - packages: - - qemu-guest-agent - - sudo - storage: - layout: - name: direct - swap: - size: 0 - user-data: - package_upgrade: false - timezone: UTC - users: - - name: root - groups: [adm, sudo] - lock-passwd: true #Disable password login - sudo: ALL=(ALL) NOPASSWD:ALL - shell: /bin/bash - passwd: "" # Remove password diff --git a/debian/13-trixie-luks/scripts/crowdsec-configuration.sh b/debian/13-trixie-luks/scripts/crowdsec-configuration.sh new file mode 100644 index 0000000..6ac3f7a --- /dev/null +++ b/debian/13-trixie-luks/scripts/crowdsec-configuration.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euf -o pipefail + +export DEBIAN_FRONTEND=noninteractive + +# Enable write-ahead-logging (wal -- allowing more concurrency in SQLite that will improve performances in most scenarios.) +sed -i -E '/^db_config:/,/^[^[:space:]]/{s/^([[:space:]]*)type:[[:space:]]*sqlite$/&\ +\1use_wal: true/}' /etc/crowdsec/config.yaml diff --git a/debian/13-trixie-luks/scripts/crowdsec-repo-setup.sh b/debian/13-trixie-luks/scripts/crowdsec-repo-setup.sh new file mode 100644 index 0000000..7b2e815 --- /dev/null +++ b/debian/13-trixie-luks/scripts/crowdsec-repo-setup.sh @@ -0,0 +1,370 @@ +#!/bin/sh +# +# Inspired from packagecloud installation scripts +# +# #MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Crowdsec repositories installation script +# +# This script: +# - Requires `root` or `sudo` privileges to run +# - Attempts to detect your Linux distribution and version and configure your +# package management system for you. +# - Installs dependencies and recommendations without asking for confirmation. +# - Is POSIX compliant and can be run using bash or any POSIX-compliant shell + + +unknown_os() { + echo "Unfortunately, your operating system distribution and version are not supported by this script." + echo + echo "You can override the OS detection by setting os= and dist= prior to running this script." + echo "You can find a list of supported OSes and distributions on our website: https://packagecloud.io/docs#os_distro_version" + echo + echo "For example, to force Ubuntu Trusty: os=ubuntu dist=trusty ./script.sh" + echo + echo "Please file an issue at https://github.com/crowdsecurity/crowdsec" + exit 1 +} + +detect_os() { + if [ -z "$os" ] && [ -z "$dist" ]; then + if [ -e /etc/os-release ]; then + . /etc/os-release + os=$ID + if [ "$os" = "poky" ]; then + dist="$VERSION_ID" + elif [ "$os" = "sles" ]; then + dist="$VERSION_ID" + os=opensuse + elif [ "$os" = "opensuse" ]; then + dist="$VERSION_ID" + elif [ "$os" = "opensuse-leap" ]; then + os=opensuse + dist="$VERSION_ID" + elif [ "$os" = "amzn" ]; then + dist="$VERSION_ID" + else + dist=$(echo "$VERSION_ID" | awk -F '.' '{ print $1 }') + fi + + elif command -v lsb_release >/dev/null; then + # get major version (e.g. '5' or '6') + dist=$(lsb_release -r | cut -f2 | awk -F '.' '{ print $1 }') + + # get os (e.g. 'centos', 'redhatenterpriseserver', etc) + os=$(lsb_release -i | cut -f2 | awk '{ print tolower($1) }') + + elif [ -e /etc/oracle-release ]; then + dist=$(cut -f5 --delimiter=' ' /etc/oracle-release | awk -F '.' '{ print $1 }') + os='ol' + + elif [ -e /etc/fedora-release ]; then + dist=$(cut -f3 --delimiter=' ' /etc/fedora-release) + os='fedora' + + elif [ -e /etc/redhat-release ]; then + os_hint=$(awk '{ print tolower($1) }' /etc/redhat-release) + if [ "$os_hint" = "centos" ]; then + dist=$(awk '{ print $3 }' /etc/redhat-release | awk -F '.' '{ print $1 }') + os='centos' + elif [ "$os_hint" = "scientific" ]; then + dist=$(awk '{ print $4 }' /etc/redhat-release | awk -F '.' '{ print $1 }') + os='scientific' + else + dist=$(awk '{ print tolower($7) }' /etc/redhat-release | cut -f1 --delimiter='.') + os='redhatenterpriseserver' + fi + + elif grep -q Amazon /etc/issue; then + dist='6' + os='aws' + else + unknown_os + fi + fi + + # remove whitespace from OS and dist name and transform to lowercase + os=$(echo "$os" | tr -d ' ' | tr '[:upper:]' '[:lower:]') + dist=$(echo "$dist" | tr -d ' ' | tr '[:upper:]' '[:lower:]') + + if [ -z "$dist" ]; then + echo "Detected operating system as $os." + else + echo "Detected operating system as $os/$dist." + fi + + if [ "$os" = "ol" ] || [ "$os" = "el" ] && [ "$dist" -gt 7 ]; then + _skip_pygpgme=1 + else + _skip_pygpgme=0 + fi + +} + +gpg_check_deb() { + echo "Checking for gpg..." + if command -v gpg >/dev/null; then + echo "Detected gpg..." + else + echo "Installing gnupg for GPG verification..." + if ! apt-get install -y gnupg; then + echo "Unable to install GPG! Your base system has a problem; please check your default OS's package repositories because GPG should work." + echo "Repository installation aborted." + echo + echo "Please file an issue at https://github.com/crowdsecurity/crowdsec" + exit 1 + fi + fi +} + +curl_check_deb() { + echo "Checking for curl..." + if command -v curl >/dev/null; then + echo "Detected curl..." + else + echo "Installing curl..." + + if apt-get install -q -y curl; then + echo "Unable to install curl! Your base system has a problem; please check your default OS's package repositories because curl should work." + echo "Repository installation aborted." + echo + echo "Please file an issue at https://github.com/crowdsecurity/crowdsec" + exit 1 + fi + fi +} + +curl_check_rpm() { + echo "Checking for curl..." + if command -v curl >/dev/null; then + echo "Detected curl..." + else + echo "Installing curl..." + yum install -d0 -e0 -y curl + fi +} + +curl_check_zypper() { + echo "Checking for curl..." + if command -v curl >/dev/null; then + echo "Detected curl..." + else + echo "Installing curl..." + zypper install curl + fi +} + +finalize_yum_repo() { + if [ "$_skip_pygpgme" = 0 ]; then + echo "Installing pygpgme to verify GPG signatures..." + yum install -y pygpgme --disablerepo="crowdsec_${repo}" + if ! rpm -qa | grep -qw pygpgme; then + echo + echo "WARNING: " + echo "The pygpgme package could not be installed. This means GPG verification is not possible for any RPM installed on your system. " + echo "To fix this, add a repository with pygpgme. Usually, the EPEL repository for your system will have this. " + echo "More information: https://fedoraproject.org/wiki/EPEL#How_can_I_use_these_extra_packages.3F" + echo + + # set the repo_gpgcheck option to 0 + sed -i'' 's/repo_gpgcheck=1/repo_gpgcheck=0/' "/etc/yum.repos.d/crowdsec_${repo}.repo" + fi + fi + + echo "Installing yum-utils..." + yum install -y yum-utils --disablerepo="crowdsec_${repo}" + if ! rpm -qa | grep -qw yum-utils; then + echo + echo "WARNING: " + echo "The yum-utils package could not be installed. This means you may not be able to install source RPMs or use other yum features." + echo + fi + + echo "Generating yum cache for crowdsec..." + yum -q makecache -y --disablerepo='*' --enablerepo="crowdsec_${repo}" +} + +install_debian_keyring() { + if [ "$os" = "debian" ]; then + echo "Installing debian-archive-keyring which is needed for installing " + echo "apt-transport-https on many Debian systems." + apt-get install -y debian-archive-keyring >/dev/null 2>&1 + fi +} + +detect_apt_version() { + apt_version_full=$(apt-get -v | head -1 | awk '{ print $2 }') + apt_version_major=$(echo "$apt_version_full" | cut -d. -f1) + apt_version_minor=$(echo "$apt_version_full" | cut -d. -f2) + apt_version_modified="${apt_version_major}${apt_version_minor}0" + + echo "Detected apt version as $apt_version_full" +} + +main() { + if [ -z "$repo" ]; then + repo="crowdsec" + fi + + detect_os + case $os in + ubuntu | debian | raspbian | linuxmint) + detect_apt_version + gpg_check_deb + curl_check_deb + apt_source_path="/etc/apt/sources.list.d/crowdsec_${repo}.list" + pre_reqs="apt-transport-https ca-certificates curl" + if [ -f "$apt_source_path" ]; then + echo + echo "The file $apt_source_path already exists: overwriting it." + echo + fi + # needed dependencies + apt-get update -qq >/dev/null + #shellcheck disable=SC2086 + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $pre_reqs >/dev/null + # gpg keys + gpg_key_url="https://packagecloud.io/crowdsec/${repo}/gpgkey" + apt_keyrings_dir="/etc/apt/keyrings" + gpg_keyring_path="$apt_keyrings_dir/crowdsec_${repo}-archive-keyring.gpg" + gpg_key_path_old="/etc/apt/trusted.gpg.d/crowdsec_${repo}.gpg" + echo + echo "Importing packagecloud gpg key... " + echo + + # move gpg key to old path if apt version is older than 1.1 + if [ "$apt_version_modified" -lt 110 ]; then + curl -fsSL "$gpg_key_url" | gpg --dearmor >"$gpg_key_path_old" + # grant 644 permisions to gpg key path old + chmod 0644 "$gpg_key_path_old" + + # deletes the keyrings directory if it is empty + echo "Packagecloud gpg key imported to $gpg_key_path_old" + else + if [ ! -d "$apt_keyrings_dir" ]; then + install -d -m 0755 "$apt_keyrings_dir" + fi + # import the gpg key + curl -fsSL "$gpg_key_url" | gpg --dearmor >"$gpg_keyring_path" + # grant 644 permisions to gpg keyring path + chmod 0644 "$gpg_keyring_path" + + echo "Packagecloud gpg key imported to $gpg_keyring_path" + fi + echo + echo "Installing ${apt_source_path}..." + echo + echo "deb [signed-by=/etc/apt/keyrings/crowdsec_${repo}-archive-keyring.gpg] https://packagecloud.io/crowdsec/${repo}/any/ any main" >"$apt_source_path" + echo "deb-src [signed-by=/etc/apt/keyrings/crowdsec_${repo}-archive-keyring.gpg] https://packagecloud.io/crowdsec/${repo}/any/ any main" >>"$apt_source_path" + apt-get update -qq >/dev/null + + ;; + centos | rhel | fedora | redhatentrepriseserver | amzn | cloudlinux | almalinux | rocky | opensuse | ol) + if [ "$os" = "ol" ] && [ "$dist" = "7" ] || [ "$os" = "amzn" ] && [ "$dist" = "2" ]; then + rpm_repo_config_url="https://packagecloud.io/install/repositories/crowdsec/${repo}/config_file.repo?os=${os}&dist=${dist}&source=script" + else + rpm_repo_config_url="https://packagecloud.io/install/repositories/crowdsec/${repo}/config_file.repo?os=rpm_any&dist=rpm_any&source=script" + fi + if [ "$os" = "opensuse" ]; then + curl_check_zypper + rpm_repo_path=/etc/zypp/repos.d/crowdsec_${repo}.repo + else + curl_check_rpm + rpm_repo_path=/etc/yum.repos.d/crowdsec_${repo}.repo + fi + + echo "Downloading repository file: $rpm_repo_config_url" + + curl -sSf "$rpm_repo_config_url" >"$rpm_repo_path" + curl_exit_code=$? + if [ "$curl_exit_code" = "22" ]; then + echo + echo + echo "Unable to download repo config from: " + echo "$rpm_repo_config_url" + echo + echo "This usually happens if your operating system is not supported by " + echo "packagecloud.io, or this script's OS detection failed." + echo + echo "You can override the OS detection by setting os= and dist= prior to running this script." + echo "You can find a list of supported OSes and distributions on our website: https://packagecloud.io/docs#os_distro_version" + echo + echo "For example, to force CentOS 6: os=el dist=6 ./script.sh" + echo + echo "If you are running a supported OS, please file an issue at https://github.com/crowdsecurity/crowdsec." + [ -e "$rpm_repo_path" ] && rm "$rpm_repo_path" + exit 1 + elif [ "$curl_exit_code" = "35" ] || [ "$curl_exit_code" = "60" ]; then + echo + echo "curl is unable to connect to packagecloud.io over TLS when running: " + echo " curl $rpm_repo_config_url" + echo + echo "This is usually due to one of two things:" + echo + echo " 1.) Missing CA root certificates (make sure the ca-certificates package is installed)" + echo " 2.) An old version of libssl. Try upgrading libssl on your system to a more recent version" + echo + echo "Contact support@crowdsec.net with information about your system for help." + [ -e "$rpm_repo_path" ] && rm "$rpm_repo_path" + exit 1 + elif [ "$curl_exit_code" -gt "0" ]; then + echo + echo "Unable to run: " + echo " curl $rpm_repo_config_url" + echo + echo "Double check your curl installation and try again." + echo + echo "Please file an issue at https://github.com/crowdsecurity/crowdsec if you think the behavior is not intended" + [ -e "$rpm_repo_path" ] && rm "$rpm_repo_path" + exit 1 + else + echo "done." + fi + if [ "$os" = "opensuse" ]; then + zypper --gpg-auto-import-keys refresh "crowdsec_${repo}" + zypper --gpg-auto-import-keys refresh "crowdsec_${repo}-source" + else + echo "$os" + finalize_yum_repo + fi + ;; + *) + echo "Error This system is not supported (yet) by this script." + echo "Please have a look at documentation https://docs.crowdsec.net/ or" + echo "file an issue at https://github.com/crowdsecurity/crowdsec if you think" + echo "the behavior is not intended" + + exit 1 + ;; + esac + + echo +} + +if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root" + echo + echo "file an issue at https://github.com/crowdsecurity/crowdsec if you think" + echo "the behavior is not intended" + exit 1 +fi + +main diff --git a/debian/13-trixie-luks/scripts/tailscale.sh b/debian/13-trixie-luks/scripts/tailscale.sh new file mode 100644 index 0000000..8ffd3f5 --- /dev/null +++ b/debian/13-trixie-luks/scripts/tailscale.sh @@ -0,0 +1,726 @@ +#!/bin/sh +# Copyright (c) Tailscale Inc & contributors +# SPDX-License-Identifier: BSD-3-Clause +# +# This script detects the current operating system, and installs +# Tailscale according to that OS's conventions. +# +# Environment variables: +# TRACK: Set to "stable" or "unstable" (default: stable) +# TAILSCALE_VERSION: Pin to a specific version (e.g., "1.88.4") +# +# Examples: +# curl -fsSL https://tailscale.com/install.sh | sh +# curl -fsSL https://tailscale.com/install.sh | TAILSCALE_VERSION=1.88.4 sh +# curl -fsSL https://tailscale.com/install.sh | TRACK=unstable sh + +set -eu + +# All the code is wrapped in a main function that gets called at the +# bottom of the file, so that a truncated partial download doesn't end +# up executing half a script. +main() { + # Step 1: detect the current linux distro, version, and packaging system. + # + # We rely on a combination of 'uname' and /etc/os-release to find + # an OS name and version, and from there work out what + # installation method we should be using. + # + # The end result of this step is that the following three + # variables are populated, if detection was successful. + OS="" + VERSION="" + PACKAGETYPE="" + APT_KEY_TYPE="" # Only for apt-based distros + APT_SYSTEMCTL_START=false # Only needs to be true for Kali + TRACK="${TRACK:-stable}" + TAILSCALE_VERSION="${TAILSCALE_VERSION:-}" + + case "$TRACK" in + stable|unstable) + ;; + *) + echo "unsupported track $TRACK" + exit 1 + ;; + esac + + if [ -f /etc/os-release ]; then + # /etc/os-release populates a number of shell variables. We care about the following: + # - ID: the short name of the OS (e.g. "debian", "freebsd") + # - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04") + # - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster") + # - UBUNTU_CODENAME: if it exists, use instead of VERSION_CODENAME + . /etc/os-release + VERSION_MAJOR="${VERSION_ID:-}" + VERSION_MAJOR="${VERSION_MAJOR%%.*}" + case "$ID" in + ubuntu|pop|neon|zorin|tuxedo) + OS="ubuntu" + if [ "${UBUNTU_CODENAME:-}" != "" ]; then + VERSION="$UBUNTU_CODENAME" + else + VERSION="$VERSION_CODENAME" + fi + PACKAGETYPE="apt" + # Third-party keyrings became the preferred method of + # installation in Ubuntu 20.04. + if [ "$VERSION_MAJOR" -lt 20 ]; then + APT_KEY_TYPE="legacy" + else + APT_KEY_TYPE="keyring" + fi + ;; + debian) + OS="$ID" + VERSION="$VERSION_CODENAME" + PACKAGETYPE="apt" + # Third-party keyrings became the preferred method of + # installation in Debian 11 (Bullseye). + if [ -z "${VERSION_ID:-}" ]; then + # rolling release. If you haven't kept current, that's on you. + APT_KEY_TYPE="keyring" + # Parrot Security is a special case that uses ID=debian + elif [ "$NAME" = "Parrot Security" ]; then + # All versions new enough to have this behaviour prefer keyring + # and their VERSION_ID is not consistent with Debian. + APT_KEY_TYPE="keyring" + # They don't specify the Debian version they're based off in os-release + # but Parrot 6 is based on Debian 12 Bookworm. + VERSION=bookworm + elif [ "$VERSION_MAJOR" -lt 11 ]; then + APT_KEY_TYPE="legacy" + else + APT_KEY_TYPE="keyring" + fi + ;; + linuxmint) + if [ "${UBUNTU_CODENAME:-}" != "" ]; then + OS="ubuntu" + VERSION="$UBUNTU_CODENAME" + elif [ "${DEBIAN_CODENAME:-}" != "" ]; then + OS="debian" + VERSION="$DEBIAN_CODENAME" + else + OS="ubuntu" + VERSION="$VERSION_CODENAME" + fi + PACKAGETYPE="apt" + if [ "$VERSION_MAJOR" -lt 5 ]; then + APT_KEY_TYPE="legacy" + else + APT_KEY_TYPE="keyring" + fi + ;; + elementary) + OS="ubuntu" + VERSION="$UBUNTU_CODENAME" + PACKAGETYPE="apt" + if [ "$VERSION_MAJOR" -lt 6 ]; then + APT_KEY_TYPE="legacy" + else + APT_KEY_TYPE="keyring" + fi + ;; + industrial-os) + OS="debian" + PACKAGETYPE="apt" + if [ "$VERSION_MAJOR" -lt 5 ]; then + VERSION="buster" + APT_KEY_TYPE="legacy" + else + VERSION="bullseye" + APT_KEY_TYPE="keyring" + fi + ;; + parrot|mendel) + OS="debian" + PACKAGETYPE="apt" + if [ "$VERSION_MAJOR" -lt 5 ]; then + VERSION="buster" + APT_KEY_TYPE="legacy" + else + VERSION="bullseye" + APT_KEY_TYPE="keyring" + fi + ;; + galliumos) + OS="ubuntu" + PACKAGETYPE="apt" + VERSION="bionic" + APT_KEY_TYPE="legacy" + ;; + pureos|kaisen) + OS="debian" + PACKAGETYPE="apt" + VERSION="bullseye" + APT_KEY_TYPE="keyring" + ;; + raspbian) + OS="$ID" + VERSION="$VERSION_CODENAME" + PACKAGETYPE="apt" + # Third-party keyrings became the preferred method of + # installation in Raspbian 11 (Bullseye). + if [ "$VERSION_MAJOR" -lt 11 ]; then + APT_KEY_TYPE="legacy" + else + APT_KEY_TYPE="keyring" + fi + ;; + kali) + OS="debian" + PACKAGETYPE="apt" + APT_SYSTEMCTL_START=true + # Third-party keyrings became the preferred method of + # installation in Debian 11 (Bullseye), which Kali switched + # to in roughly 2021.x releases + if [ "$VERSION_MAJOR" -lt 2021 ]; then + # Kali VERSION_ID is "kali-rolling", which isn't distinguishing + VERSION="buster" + APT_KEY_TYPE="legacy" + else + VERSION="bullseye" + APT_KEY_TYPE="keyring" + fi + ;; + Deepin|deepin) # https://github.com/tailscale/tailscale/issues/7862 + OS="debian" + PACKAGETYPE="apt" + if [ "$VERSION_MAJOR" -lt 20 ]; then + APT_KEY_TYPE="legacy" + VERSION="buster" + else + APT_KEY_TYPE="keyring" + VERSION="bullseye" + fi + ;; + pika) + PACKAGETYPE="apt" + # All versions of PikaOS are new enough to prefer keyring + APT_KEY_TYPE="keyring" + # Older versions of PikaOS are based on Ubuntu rather than Debian + if [ "$VERSION_MAJOR" -lt 4 ]; then + OS="ubuntu" + VERSION="$UBUNTU_CODENAME" + else + OS="debian" + VERSION="$DEBIAN_CODENAME" + fi + ;; + sparky) + OS="debian" + PACKAGETYPE="apt" + VERSION="$DEBIAN_CODENAME" + APT_KEY_TYPE="keyring" + ;; + centos) + OS="$ID" + VERSION="$VERSION_MAJOR" + PACKAGETYPE="dnf" + if [ "$VERSION" = "7" ]; then + PACKAGETYPE="yum" + fi + ;; + ol) + OS="oracle" + VERSION="$VERSION_MAJOR" + PACKAGETYPE="dnf" + if [ "$VERSION" = "7" ]; then + PACKAGETYPE="yum" + fi + ;; + rhel|miraclelinux) + OS="$ID" + if [ "$ID" = "miraclelinux" ]; then + OS="rhel" + fi + VERSION="$VERSION_MAJOR" + PACKAGETYPE="dnf" + if [ "$VERSION" = "7" ]; then + PACKAGETYPE="yum" + fi + ;; + fedora) + OS="$ID" + VERSION="" + PACKAGETYPE="dnf" + ;; + rocky|almalinux|nobara|openmandriva|sangoma|risios|cloudlinux|alinux|fedora-asahi-remix|ultramarine) + OS="fedora" + VERSION="" + PACKAGETYPE="dnf" + ;; + amzn) + OS="amazon-linux" + VERSION="$VERSION_ID" + PACKAGETYPE="yum" + ;; + xenenterprise) + OS="centos" + VERSION="$VERSION_MAJOR" + PACKAGETYPE="yum" + ;; + opensuse-leap|sles) + OS="opensuse" + VERSION="leap/$VERSION_ID" + PACKAGETYPE="zypper" + ;; + opensuse-tumbleweed) + OS="opensuse" + VERSION="tumbleweed" + PACKAGETYPE="zypper" + ;; + sle-micro-rancher) + OS="opensuse" + VERSION="leap/15.4" + PACKAGETYPE="zypper" + ;; + arch|archarm|endeavouros|blendos|garuda|archcraft|cachyos) + OS="arch" + VERSION="" # rolling release + PACKAGETYPE="pacman" + ;; + manjaro|manjaro-arm|biglinux) + OS="manjaro" + VERSION="" # rolling release + PACKAGETYPE="pacman" + ;; + alpine) + OS="$ID" + VERSION="$VERSION_ID" + PACKAGETYPE="apk" + ;; + postmarketos) + OS="alpine" + VERSION="$VERSION_ID" + PACKAGETYPE="apk" + ;; + nixos) + echo "Please add Tailscale to your NixOS configuration directly:" + echo + echo "services.tailscale.enable = true;" + exit 1 + ;; + bazzite) + echo "Bazzite comes with Tailscale installed by default." + echo "Please enable Tailscale by running the following commands as root:" + echo + echo "ujust enable-tailscale" + echo "tailscale up" + exit 1 + ;; + void) + OS="$ID" + VERSION="" # rolling release + PACKAGETYPE="xbps" + ;; + gentoo) + OS="$ID" + VERSION="" # rolling release + PACKAGETYPE="emerge" + ;; + freebsd) + OS="$ID" + VERSION="$VERSION_MAJOR" + PACKAGETYPE="pkg" + ;; + osmc) + OS="debian" + PACKAGETYPE="apt" + VERSION="bullseye" + APT_KEY_TYPE="keyring" + ;; + photon) + OS="photon" + VERSION="$VERSION_MAJOR" + PACKAGETYPE="tdnf" + ;; + steamos) + echo "To install Tailscale on SteamOS, please follow the instructions here:" + echo "https://github.com/tailscale-dev/deck-tailscale" + exit 1 + ;; + + # TODO: wsl? + # TODO: synology? qnap? + esac + fi + + # If we failed to detect something through os-release, consult + # uname and try to infer things from that. + if [ -z "$OS" ]; then + if type uname >/dev/null 2>&1; then + case "$(uname)" in + FreeBSD) + # FreeBSD before 12.2 doesn't have + # /etc/os-release, so we wouldn't have found it in + # the os-release probing above. + OS="freebsd" + VERSION="$(freebsd-version | cut -f1 -d.)" + PACKAGETYPE="pkg" + ;; + OpenBSD) + OS="openbsd" + VERSION="$(uname -r)" + PACKAGETYPE="" + ;; + Darwin) + OS="macos" + VERSION="$(sw_vers -productVersion | cut -f1-2 -d.)" + PACKAGETYPE="appstore" + ;; + Linux) + OS="other-linux" + VERSION="" + PACKAGETYPE="" + ;; + esac + fi + fi + + # Ideally we want to use curl, but on some installs we + # only have wget. Detect and use what's available. + CURL= + if type curl >/dev/null; then + CURL="curl -fsSL" + elif type wget >/dev/null; then + CURL="wget -q -O-" + fi + if [ -z "$CURL" ]; then + echo "The installer needs either curl or wget to download files." + echo "Please install either curl or wget to proceed." + exit 1 + fi + + TEST_URL="https://pkgs.tailscale.com/" + RC=0 + TEST_OUT=$($CURL "$TEST_URL" 2>&1) || RC=$? + if [ $RC != 0 ]; then + echo "The installer cannot reach $TEST_URL" + echo "Please make sure that your machine has internet access." + echo "Test output:" + echo $TEST_OUT + exit 1 + fi + + # Step 2: having detected an OS we support, is it one of the + # versions we support? + OS_UNSUPPORTED= + case "$OS" in + ubuntu|debian|raspbian|centos|oracle|rhel|amazon-linux|opensuse|photon) + # Check with the package server whether a given version is supported. + URL="https://pkgs.tailscale.com/$TRACK/$OS/$VERSION/installer-supported" + $CURL "$URL" 2> /dev/null | grep -q OK || OS_UNSUPPORTED=1 + ;; + fedora) + # All versions supported, no version checking required. + ;; + arch) + # Rolling release, no version checking needed. + ;; + manjaro) + # Rolling release, no version checking needed. + ;; + alpine) + # All versions supported, no version checking needed. + # TODO: is that true? When was tailscale packaged? + ;; + void) + # Rolling release, no version checking needed. + ;; + gentoo) + # Rolling release, no version checking needed. + ;; + freebsd) + if [ "$VERSION" != "12" ] && \ + [ "$VERSION" != "13" ] && \ + [ "$VERSION" != "14" ] && \ + [ "$VERSION" != "15" ] + then + OS_UNSUPPORTED=1 + fi + ;; + openbsd) + OS_UNSUPPORTED=1 + ;; + macos) + # We delegate macOS installation to the app store, it will + # perform version checks for us. + ;; + other-linux) + OS_UNSUPPORTED=1 + ;; + *) + OS_UNSUPPORTED=1 + ;; + esac + if [ "$OS_UNSUPPORTED" = "1" ]; then + case "$OS" in + other-linux) + echo "Couldn't determine what kind of Linux is running." + echo "You could try the static binaries at:" + echo "https://pkgs.tailscale.com/$TRACK/#static" + ;; + "") + echo "Couldn't determine what operating system you're running." + ;; + *) + echo "$OS $VERSION isn't supported by this script yet." + ;; + esac + echo + echo "If you'd like us to support your system better, please email support@tailscale.com" + echo "and tell us what OS you're running." + echo + echo "Please include the following information we gathered from your system:" + echo + echo "OS=$OS" + echo "VERSION=$VERSION" + echo "PACKAGETYPE=$PACKAGETYPE" + if type uname >/dev/null 2>&1; then + echo "UNAME=$(uname -a)" + else + echo "UNAME=" + fi + echo + if [ -f /etc/os-release ]; then + cat /etc/os-release + else + echo "No /etc/os-release" + fi + exit 1 + fi + + # Step 3: work out if we can run privileged commands, and if so, + # how. + CAN_ROOT= + SUDO= + if [ "$(id -u)" = 0 ]; then + CAN_ROOT=1 + SUDO="" + elif type sudo >/dev/null; then + CAN_ROOT=1 + SUDO="sudo" + elif type doas >/dev/null; then + CAN_ROOT=1 + SUDO="doas" + fi + if [ "$CAN_ROOT" != "1" ]; then + echo "This installer needs to run commands as root." + echo "We tried looking for 'sudo' and 'doas', but couldn't find them." + echo "Either re-run this script as root, or set up sudo/doas." + exit 1 + fi + + + # Step 4: run the installation. + OSVERSION="$OS" + [ "$VERSION" != "" ] && OSVERSION="$OSVERSION $VERSION" + + # Prepare package name with optional version + PACKAGE_NAME="tailscale" + if [ -n "$TAILSCALE_VERSION" ]; then + echo "Installing Tailscale $TAILSCALE_VERSION for $OSVERSION, using method $PACKAGETYPE" + else + echo "Installing Tailscale for $OSVERSION, using method $PACKAGETYPE" + fi + case "$PACKAGETYPE" in + apt) + export DEBIAN_FRONTEND=noninteractive + if [ "$APT_KEY_TYPE" = "legacy" ] && ! type gpg >/dev/null; then + $SUDO apt-get update + $SUDO apt-get install -y gnupg + fi + + set -x + $SUDO mkdir -p --mode=0755 /usr/share/keyrings + case "$APT_KEY_TYPE" in + legacy) + $CURL "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION.asc" | $SUDO apt-key add - + $CURL "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION.list" | $SUDO tee /etc/apt/sources.list.d/tailscale.list + $SUDO chmod 0644 /etc/apt/sources.list.d/tailscale.list + ;; + keyring) + $CURL "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION.noarmor.gpg" | $SUDO tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null + $SUDO chmod 0644 /usr/share/keyrings/tailscale-archive-keyring.gpg + $CURL "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION.tailscale-keyring.list" | $SUDO tee /etc/apt/sources.list.d/tailscale.list + $SUDO chmod 0644 /etc/apt/sources.list.d/tailscale.list + ;; + esac + $SUDO apt-get update + if [ -n "$TAILSCALE_VERSION" ]; then + $SUDO apt-get install -y "tailscale=$TAILSCALE_VERSION" tailscale-archive-keyring + else + $SUDO apt-get install -y tailscale tailscale-archive-keyring + fi + if [ "$APT_SYSTEMCTL_START" = "true" ]; then + $SUDO systemctl enable --now tailscaled + $SUDO systemctl start tailscaled + fi + set +x + ;; + yum) + set -x + $SUDO yum install yum-utils -y + $SUDO yum-config-manager -y --add-repo "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION/tailscale.repo" + if [ -n "$TAILSCALE_VERSION" ]; then + $SUDO yum install "tailscale-$TAILSCALE_VERSION" -y + else + $SUDO yum install tailscale -y + fi + $SUDO systemctl enable --now tailscaled + set +x + ;; + dnf) + # DNF 5 has a different argument format; determine which one we have. + DNF_VERSION="3" + if LANG=C.UTF-8 dnf --version | grep -q '^dnf5 version'; then + DNF_VERSION="5" + fi + + # The 'config-manager' plugin wasn't implemented when + # DNF5 was released; detect that and use the old + # version if necessary. + if [ "$DNF_VERSION" = "5" ]; then + set -x + $SUDO dnf install -y 'dnf-command(config-manager)' && DNF_HAVE_CONFIG_MANAGER=1 || DNF_HAVE_CONFIG_MANAGER=0 + set +x + + if [ "$DNF_HAVE_CONFIG_MANAGER" != "1" ]; then + if type dnf-3 >/dev/null; then + DNF_VERSION="3" + else + echo "dnf 5 detected, but 'dnf-command(config-manager)' not available and dnf-3 not found" + exit 1 + fi + fi + fi + + set -x + if [ "$DNF_VERSION" = "3" ]; then + $SUDO dnf install -y 'dnf-command(config-manager)' + $SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION/tailscale.repo" + elif [ "$DNF_VERSION" = "5" ]; then + # Already installed config-manager, above. + $SUDO dnf config-manager addrepo --overwrite --from-repofile="https://pkgs.tailscale.com/$TRACK/$OS/$VERSION/tailscale.repo" + else + echo "unexpected: unknown dnf version $DNF_VERSION" + exit 1 + fi + if [ -n "$TAILSCALE_VERSION" ]; then + $SUDO dnf install -y "tailscale-$TAILSCALE_VERSION" + else + $SUDO dnf install -y tailscale + fi + $SUDO systemctl enable --now tailscaled + set +x + ;; + tdnf) + set -x + curl -fsSL "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION/tailscale.repo" > /etc/yum.repos.d/tailscale.repo + if [ -n "$TAILSCALE_VERSION" ]; then + $SUDO tdnf install -y "tailscale-$TAILSCALE_VERSION" + else + $SUDO tdnf install -y tailscale + fi + $SUDO systemctl enable --now tailscaled + set +x + ;; + zypper) + set -x + $SUDO rpm --import "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION/repo.gpg" + $SUDO zypper --non-interactive ar -g -r "https://pkgs.tailscale.com/$TRACK/$OS/$VERSION/tailscale.repo" + $SUDO zypper --non-interactive --gpg-auto-import-keys refresh + if [ -n "$TAILSCALE_VERSION" ]; then + $SUDO zypper --non-interactive install "tailscale=$TAILSCALE_VERSION" + else + $SUDO zypper --non-interactive install tailscale + fi + $SUDO systemctl enable --now tailscaled + set +x + ;; + pacman) + set -x + if [ -n "$TAILSCALE_VERSION" ]; then + echo "Warning: Arch Linux maintains their own Tailscale package. Version pinning may not work as expected, as the target version may no longer be available." + $SUDO pacman -S "tailscale=$TAILSCALE_VERSION" --noconfirm + else + $SUDO pacman -S tailscale --noconfirm + fi + $SUDO systemctl enable --now tailscaled + set +x + ;; + pkg) + set -x + if [ -n "$TAILSCALE_VERSION" ]; then + echo "Warning: FreeBSD maintains their own Tailscale package. Version pinning may not work as expected, as the target version may no longer be available." + $SUDO pkg install --yes "tailscale-$TAILSCALE_VERSION" + else + $SUDO pkg install --yes tailscale + fi + $SUDO service tailscaled enable + $SUDO service tailscaled start + set +x + ;; + apk) + set -x + if ! grep -Eq '^http.*/community$' /etc/apk/repositories; then + if type setup-apkrepos >/dev/null; then + $SUDO setup-apkrepos -c -1 + else + echo "installing tailscale requires the community repo to be enabled in /etc/apk/repositories" + exit 1 + fi + fi + if [ -n "$TAILSCALE_VERSION" ]; then + echo "Warning: Alpine Linux maintains their own Tailscale package. Version pinning may not work as expected, as the target version may no longer be available." + $SUDO apk add "tailscale=$TAILSCALE_VERSION" + else + $SUDO apk add tailscale + fi + $SUDO rc-update add tailscale + $SUDO rc-service tailscale start + set +x + ;; + xbps) + set -x + if [ -n "$TAILSCALE_VERSION" ]; then + echo "Warning: Void Linux maintains their own Tailscale package. Version pinning may not work as expected, as the target version may no longer be available." + $SUDO xbps-install "tailscale-$TAILSCALE_VERSION" -y + else + $SUDO xbps-install tailscale -y + fi + set +x + ;; + emerge) + set -x + if [ -n "$TAILSCALE_VERSION" ]; then + echo "Warning: Gentoo maintains their own Tailscale package. Version pinning may not work as expected, as the target version may no longer be available." + $SUDO emerge --ask=n "=net-vpn/tailscale-$TAILSCALE_VERSION" + else + $SUDO emerge --ask=n net-vpn/tailscale + fi + set +x + ;; + appstore) + set -x + open "https://apps.apple.com/us/app/tailscale/id1475387142" + set +x + ;; + *) + echo "unexpected: unknown package type $PACKAGETYPE" + exit 1 + ;; + esac + + echo "Installation complete! Log in to start using Tailscale by running:" + echo + if [ -z "$SUDO" ]; then + echo "tailscale up" + else + echo "$SUDO tailscale up" + fi +} + +main