{"id":339,"date":"2026-09-19T20:06:31","date_gmt":"2026-09-19T18:06:31","guid":{"rendered":"https:\/\/tillnet.se\/?p=339"},"modified":"2026-09-19T20:10:08","modified_gmt":"2026-09-19T18:10:08","slug":"ubuntu-vm-bootstrap","status":"publish","type":"post","link":"https:\/\/tillnet.se\/?p=339","title":{"rendered":"Ubuntu-vm-bootstrap"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Every new Ubuntu VM on the XCP-ng pool needed the same post-install housekeeping \u2014 updates, timezone, NTP, guest tools, a few baseline packages \u2014 done by hand, every time. Instead of repeating that checklist again, I put together a bash script that does it in one pass and only touches the system when told to.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/github.com\/tobiastillstam\/Ubuntu-vm-bootstrap\">https:\/\/github.com\/tobiastillstam\/Ubuntu-vm-bootstrap<\/a><\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Post-install housekeeping for a freshly installed Ubuntu VM. Detects current state and reports it; only changes anything when <code>--fix<\/code> is given (always previewable via <code>--dry-run<\/code>).<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Built for XCP-ng guests first, but it also detects and handles KVM\/Proxmox, VMware, Hyper-V, and VirtualBox. Interactive with a terminal attached (a yes\/no + confirm-the-default wizard), flag-driven and silent otherwise \u2014 including a straight <code>curl | bash<\/code> pipe.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -fsSL https:\/\/raw.githubusercontent.com\/tobiastillstam\/Ubuntu-vm-bootstrap\/main\/ubuntu-vm-bootstrap.sh | sudo bash\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Features<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Audit-first<\/strong> \u2014 a plain run only reports what it would do. Nothing changes until <code>--fix<\/code> is given, and <code>--dry-run<\/code> previews either way.<\/li>\n\n\n\n<li><strong>Core housekeeping<\/strong> \u2014 full <code>apt update\/full-upgrade\/autoremove\/autoclean<\/code>, timezone + NTP via <code>systemd-timesyncd<\/code>, and a set of baseline CLI packages (curl, git, jq, htop, tmux, and the rest).<\/li>\n\n\n\n<li><strong>Hypervisor-aware guest tools<\/strong> \u2014 auto-detects the hypervisor via <code>systemd-detect-virt<\/code> and installs the matching guest tools, or takes an override with <code>--hypervisor<\/code>.<\/li>\n\n\n\n<li><strong>Interactive wizard or fully unattended<\/strong> \u2014 asks its way through the settings when a terminal is attached; runs silently under <code>--yes<\/code>, automation, or a piped install.<\/li>\n\n\n\n<li><strong>Opt-in SSH hardening (<code>--harden<\/code>)<\/strong> \u2014 key-only SSH (root login and password auth disabled), plus UFW with an OpenSSH allow rule. Refuses to lock out password auth if no <code>authorized_keys<\/code> is found, unless forced.<\/li>\n\n\n\n<li><strong>Opt-in swap, unattended-upgrades, and Zabbix Agent2<\/strong> \u2014 each behind its own flag (<code>--swap<\/code>, <code>--unattended-upgrades<\/code>, <code>--zabbix<\/code>), with the generated Zabbix PSK printed once and never written to a log.<\/li>\n\n\n\n<li><strong>Idempotent<\/strong> \u2014 safe to run again on the same VM. Verified live on Ubuntu 26.04 LTS on XCP-ng across audit, dry-run, a full <code>--fix<\/code> with every optional category, and a second pass to confirm nothing changes twice.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Every new Ubuntu VM on the XCP-ng pool needed the same post-install housekeeping \u2014 updates, timezone, NTP, guest tools, a few baseline packages \u2014 done &hellip;<\/p>\n","protected":false},"author":1,"featured_media":340,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[],"class_list":["post-339","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-it"],"jetpack_featured_media_url":"https:\/\/tillnet.se\/wp-content\/uploads\/2026\/09\/Gemini_Generated_Image_6cw2fz6cw2fz6cw2.jpeg","_links":{"self":[{"href":"https:\/\/tillnet.se\/index.php?rest_route=\/wp\/v2\/posts\/339","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tillnet.se\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tillnet.se\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tillnet.se\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tillnet.se\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=339"}],"version-history":[{"count":2,"href":"https:\/\/tillnet.se\/index.php?rest_route=\/wp\/v2\/posts\/339\/revisions"}],"predecessor-version":[{"id":342,"href":"https:\/\/tillnet.se\/index.php?rest_route=\/wp\/v2\/posts\/339\/revisions\/342"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tillnet.se\/index.php?rest_route=\/wp\/v2\/media\/340"}],"wp:attachment":[{"href":"https:\/\/tillnet.se\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=339"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tillnet.se\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=339"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tillnet.se\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=339"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}