Files
Ansible-Bootstrap/README.md

37 KiB

Ansible Bootstrap

Automated Linux system bootstrap using the Arch Linux ISO as a universal installer. Deploys any supported distribution on virtual or physical targets via Infrastructure-as-Code.

Non-Arch targets require the appropriate package manager available from the ISO environment (e.g. dnf for RHEL-family). Set system.features.chroot.tool if arch-chroot is unavailable.

Table of Contents

  1. Supported Platforms
  2. Compatibility Notes
  3. Configuration Model
  4. Variable Reference
  5. Execution Pipeline
  6. Usage
  7. Security
  8. Safety

1. Supported Platforms

Distributions

system.os Distribution system.version
almalinux AlmaLinux 9, 10
archlinux Arch Linux latest (rolling)
debian Debian 12, 13, unstable
fedora Fedora 43, 44
rhel Red Hat Enterprise Linux 9, 10
rocky Rocky Linux 9, 10
ubuntu Ubuntu (latest non-LTS) optional (tracks 25.10 questing)
ubuntu-lts Ubuntu LTS optional (tracks 26.04 resolute)

Hypervisors

Hypervisor hypervisor.type
libvirt libvirt
Proxmox VE proxmox
VMware vmware
Xen xen
Bare metal none

2. Compatibility Notes

  • rhel_iso is required for system.os: rhel.
  • RHEL installs should use ext4 or xfs (not btrfs).
  • custom_iso: true skips ArchISO validation; your installer must provide required tooling.
  • On non-Arch installers, set system.features.chroot.tool explicitly.

3. Configuration Model

Two dict-based variables drive the entire configuration:

  • system -- host, network, users, disk layout, encryption, and feature toggles (including CIS hardening under system.features.cis)
  • hypervisor -- virtualization backend credentials and targeting

Both are standard Ansible variables. Place them in group_vars/, host_vars/, or inline inventory. With hash_behaviour = merge, dictionaries merge across scopes, so shared values go in group vars and host-specific overrides go per-host.

Variable Placement

Location Scope Typical use
group_vars/all.yml All hosts Shared hypervisor, system.filesystem, boot_iso
group_vars/<group>.yml Group Environment-specific defaults
host_vars/<host>.yml Single host Host-specific overrides (system.network.ip, system.id, etc.)

Example Inventory

all:
  vars:
    system:
      filesystem: btrfs
    boot_iso: "local:iso/archlinux-x86_64.iso"
    hypervisor:
      type: proxmox
      url: pve01.example.com
      username: root@pam
      password: !vault |
        $ANSIBLE_VAULT...
      node: pve01
      storage: local-lvm

  children:
    bootstrap:
      hosts:
        app01.example.com:
          ansible_host: 10.0.0.10
          system:
            type: virtual
            os: debian
            version: "12"
            name: app01.example.com
            id: 101
            cpus: 2
            memory: 4096
            network:
              bridge: vmbr0
              ip: 10.0.0.10
              prefix: 24
              gateway: 10.0.0.1
              dns:
                servers: [1.1.1.1, 1.0.0.1]
                search: [example.com]
            disks:
              - size: 40
              - size: 120
                mount:
                  path: /data
                  fstype: xfs
            users:
              ops:
                password: !vault |
                  $ANSIBLE_VAULT...
                keys:
                  - "ssh-ed25519 AAAA..."
                sudo: true
            root:
              password: !vault |
                $ANSIBLE_VAULT...
            luks:
              enabled: true
              passphrase: !vault |
                $ANSIBLE_VAULT...
              method: tpm2
              tpm2:
                pcrs: "7"
            features:
              cis:
                enabled: true
              firewall:
                enabled: true
                backend: firewalld
                toolkit: nftables

4. Variable Reference

4.1 Core Variables

Top-level variables outside system/hypervisor.

Variable Type Default Description
boot_iso string -- Boot ISO path (required for virtual installs)
rhel_iso string -- RHEL ISO path (required when system.os: rhel)
custom_iso bool false Skip ArchISO validation and pacman setup
thirdparty_tasks string dropins/preparation.yml Drop-in task file included during environment setup

4.2 system Dictionary

Key Type Default Description
type string virtual virtual or physical
os string -- Target distribution (see table)
version string -- Version selector for versioned distros
filesystem string ext4 btrfs, ext4, or xfs
name string inventory hostname Final hostname
timezone string Europe/Vienna System timezone (tz database name)
locale string en_US.UTF-8 System locale
keymap string us Console keymap
id int/string -- VMID (required for Proxmox)
cpus int 0 vCPU count (required for virtual)
memory int 0 Memory in MiB (required for virtual)
balloon int 0 Balloon memory in MiB (Proxmox)
path string -- Hypervisor folder/path (falls back to hypervisor.folder)
content dict see below Package content source (mirror/DVD/Satellite, family-resolved)
packages list [] Additional packages installed post-reboot
network dict see below Network configuration
disks list [] Disk layout (see Multi-Disk Schema)
users dict {} User accounts (keyed by username)
root dict see below Root account settings
luks dict see below Encryption settings
features dict see below Feature toggles

system.content

Uniform package content source, family-resolved. source: '' defaults to dvd on EL and mirror on Debian/Ubuntu/Arch. Satellite values come from inventory/vault only, never committed code.

Key Type Default Description
source string family default dvd, mirror, satellite, or none
url string family default Mirror URL / EL .repo baseurl
proxy string -- http://host:port content proxy (dnf/apt/pacman)
gpgcheck bool true Repository GPG checking
satellite.host string -- EL Katello/Satellite hostname
satellite.ip string -- Optional /etc/hosts entry when DNS does not resolve the host
satellite.org string -- Organization label
satellite.activation_key string -- Activation key
satellite.ca_url string derived Katello CA RPM URL (default https://<host>/pub/katello-ca-consumer-latest.noarch.rpm)
satellite.service_level string -- syspurpose service level
satellite.environment string -- Lifecycle environment
satellite.install bool false false: base from DVD/mirror then register; true: install from Satellite

system.network

Key Type Default Description
bridge string -- Hypervisor network/bridge name
vlan string/int -- VLAN tag
ip string -- Static IP (omit for DHCP)
prefix int -- CIDR prefix (1-32, required with ip)
gateway string -- Default gateway
dns.servers list [] DNS resolvers (must be a YAML list)
dns.search list [] Search domains (must be a YAML list)
interfaces list [] Multi-NIC config (overrides flat fields above)

When interfaces is empty, the flat fields (bridge, ip, prefix, gateway, vlan) are auto-wrapped into a single-entry list. When interfaces is set, it takes precedence. Each entry supports: name, bridge (required), vlan, ip, prefix, gateway.

system.users

Dict keyed by username. At least one user must have a password (used for SSH access during bootstrap). Users without a password get locked accounts (key-only auth).

system:
  users:
    svcansible:
      password: "vault_lookup"
      keys:
        - "ssh-ed25519 AAAA..."
    appuser:
      sudo: "ALL=(ALL) NOPASSWD: ALL"
      keys:
        - "ssh-ed25519 BBBB..."
Key Type Default Description
(dict key) string -- Username (required)
password string -- User password (required for at least one user)
keys list [] SSH public keys
sudo bool/string -- true for NOPASSWD ALL, or custom sudoers string

Users must be defined in inventory. The dict format enables additive merging across inventory layers with hash_behaviour=merge.

system.root

Key Type Default Description
password string -- Root password
shell string /bin/bash Login shell

system.luks

Key Type Default Description
enabled bool false Enable encrypted root
passphrase string -- Passphrase for format/open/enroll
mapper string SYSTEM_DECRYPTED Mapper name under /dev/mapper
auto bool true Auto-unlock toggle
method string tpm2 Auto-unlock backend: tpm2 or keyfile
keysize int 64 Keyfile size in bytes
options string discard,tries=3 Additional crypttab options
type string luks2 LUKS format type
cipher string aes-xts-plain64 Cipher
hash string sha512 Hash algorithm
iter int 4000 PBKDF iteration time (ms)
bits int 512 Key size (bits)
pbkdf string argon2id PBKDF algorithm

system.luks.tpm2

Key Type Default Description
device string auto TPM2 device selector
pcrs string/list -- PCR binding policy (e.g. "7" or "0+7"); empty = no PCR binding

TPM2 auto-unlock: Uses systemd-cryptenroll on all distros. The user-set passphrase remains as a backup unlock method. TPM2 enrollment runs in the chroot during bootstrap; if it fails (e.g. no TPM2 hardware), the system boots with passphrase-only unlock and TPM2 can be enrolled post-deployment via systemd-cryptenroll --tpm2-device=auto <device>.

On Debian/Ubuntu, TPM2 auto-unlock requires dracut (initramfs-tools does not support tpm2-device). The bootstrap auto-switches to dracut when method: tpm2 is set. Override via features.initramfs.generator.

system.features

Key Type Default Description
cis.enabled bool false Enable CIS hardening (see 4.4)
cis.profile string default CIS profile: default, l1, or l2 (see 4.4)
cis.rules dict {} Per-rule CIS overrides
cis.params dict {} CIS parameter overrides
selinux.enabled bool true SELinux management
firewall.enabled bool true Firewall setup
firewall.backend string firewalld firewalld or ufw
firewall.toolkit string nftables nftables or iptables
ssh.enabled bool true SSH service/package management
zstd.enabled bool true zstd-related tuning
swap.enabled bool true Swap setup
banner.motd bool false MOTD banner
banner.sudo bool true Sudo banner
chroot.tool string arch-chroot arch-chroot, chroot, or systemd-nspawn
initramfs.generator string auto-detected Override initramfs generator (see below)
secure_boot.enabled bool false Enable Secure Boot (Arch via sbctl, others via shim)
secure_boot.method string -- Arch only: sbctl (default) or uki
desktop.* dict see below Desktop environment settings (see 4.2.5)
firmware.* dict see below Vendor firmware blobs and CPU microcode (see 4.2.6)
gpu.* dict see below Mesa/Vulkan and per-vendor GPU userspace (see 4.2.7)
peripherals.* dict see below Fingerprint, camera, audio, bluetooth, DisplayLink (see 4.2.8)
hardware.* dict see below Hardware-detection profile override (see 4.2.9)

Initramfs generator auto-detection: RedHat -> dracut, Arch -> mkinitcpio, Debian/Ubuntu -> initramfs-tools. Override with dracut, mkinitcpio, or initramfs-tools. When LUKS TPM2 auto-unlock is enabled and the native generator does not support tpm2-device, the generator is automatically upgraded to dracut. On distros with older dracut (no tpm2-tss module), clevis is used as a fallback for TPM2 binding.

4.2.5 system.features.desktop

Key Type Default Description
enabled bool false Install desktop environment
environment string "" gnome, kde, sway, or hyprland
display_manager string auto-detected Override DM: gdm, sddm, plasma-login-manager, greetd, or ly
autologin bool | string false false to disable, or a username from system.users to auto-login that user
session string auto-from-environment Session to autologin into; overrides the per-environment default (sddm .desktop basename / greetd command)
groups list [] Opt-in package groups installed on top of the base set (keys of desktop_package_groups, e.g. dev)

All desktop environments are Wayland-only. sway and hyprland are available on Arch only; gnome and kde are available on all three families. On enterprise Linux (almalinux/rocky/rhel) the base desktop installs browser, PDF and image viewers but no video player - none is packaged in the EL base repositories, and no third-party repo is pulled in; add one from rpmfusion/flatpak if you need it.

When enabled: true, the bootstrap installs the desktop environment packages, enables the display manager and bluetooth services, and sets the systemd default target to graphical.target.

Display manager auto-detection: gnome to gdm; kde to plasma-login-manager on Arch and Fedora 44+ (Plasma 6.6), else sddm; sway and hyprland to greetd.

ly is an explicit-only override (never auto-selected), available on Arch only, and is desktop-agnostic - it can front any environment. It runs on tty2 with getty@tty2 masked, and its autologin is written to /etc/ly/config.ini; set session to the target session's .desktop basename (sway and hyprland resolve automatically).

When autologin names a user, the matching display manager is configured to log that user in without a password prompt. session is resolved automatically per environment when left empty (gdm picks its default, sddm uses plasma.desktop for kde, greetd runs the compositor command for sway/hyprland), so it only needs setting to override that choice.

4.2.6 system.features.firmware

Key Type Default Description
enabled bool | auto auto Install vendor firmware blobs. auto = on for physical, off for virtual
microcode bool | auto auto Install CPU microcode. auto follows firmware.enabled

Defaults are designed so a baremetal install picks up firmware automatically with no inventory entry needed, while VMs skip it (the hypervisor handles those). The environment role detects CPU/GPU/wireless vendors from the live host (via lscpu and lspci) and the bootstrap role installs only the matching firmware packages. On Arch, this uses the vendor splits (linux-firmware-amdgpu, linux-firmware-realtek, etc.) so the install stays minimal. On Debian, it uses the equivalent firmware-* packages. Distros without firmware splits fall back to a single meta package.

4.2.7 system.features.gpu

Key Type Default Description
enabled bool false Install Mesa, Vulkan, and per-GPU userspace
nvidia_driver string auto One of auto, open, proprietary, nouveau

Pair with desktop.enabled: true for a working desktop. The package set is determined by the same hardware profile as firmware. The nvidia_driver: auto default picks open (nvidia-open kernel modules) for Turing or newer GPUs, falls back to proprietary for older cards on distros that ship the proprietary driver, and falls back to nouveau elsewhere. Force a specific flavor by setting the value explicitly.

Proprietary and open Nvidia drivers on Fedora require RPMFusion non-free, which the bootstrap enables automatically when needed. Debian uses nvidia-driver from the non-free component (already enabled in the managed sources.list). Ubuntu uses restricted. Arch ships both nvidia-open-dkms and nvidia-dkms in the extra repository - no third-party setup required.

Known limitation - Nvidia on Enterprise Linux (AlmaLinux/Rocky/RHEL): the EL akmod-nvidia* packages live in RPMFusion non-free, and the bootstrap only enables RPMFusion automatically on Fedora, not on EL. So Nvidia on a bare EL desktop is best-effort: enable RPMFusion (or supply the driver repo) out of band, or it falls back to nouveau. EL desktops are not a primary target.

4.2.8 system.features.peripherals

Key Type Default Description
enabled bool | auto auto Master switch. auto follows desktop.enabled
fingerprint bool | auto auto fprintd/libfprint. auto = install when reader detected
camera bool | auto auto v4l-utils for UVC webcams. auto = install when a UVC/IPU6 camera is detected (IPU6 out-of-tree stack is logged, not auto-installed)
audio bool | auto auto SOF firmware + ALSA UCM. auto = install when an audio device is detected
bluetooth bool | auto auto bluez. auto = install when a Bluetooth controller is detected
displaylink bool false DisplayLink dock support (explicit opt-in; see notes)

Fingerprint detection scans lsusb for known reader vendor IDs (Synaptics, Validity, Goodix, Elan, Egis, Broadcom, AuthenTec, Upek, Futronic). When fingerprint: auto and a reader is present, fprintd and the PAM helper are installed. PAM enrollment must be done post-install (fprintd-enroll).

DisplayLink ships proprietary userspace that distros do not package consistently. The bootstrap installs the in-tree evdi-dkms kernel module on Debian/Ubuntu and the evdi module on Fedora, but the userspace blob must still be installed manually from DisplayLink's site after first boot. Arch users typically use AUR (displaylink); this is not wired into the bootstrap.

4.2.9 system.features.hardware

Key Type Default Description
profile dict {} Full override: non-empty SKIPS detection (golden image); empty = autodetect
group fields mixed -- cpu/gpus/wireless/audio/camera/fingerprint/bluetooth/packages/disable/kernel_params MERGE over autodetect (see below)

When empty, hardware is detected at the start of the bootstrap. When set, detection is skipped and the supplied profile drives package selection - this is the golden-image flow: bake an image with a fixed profile, snapshot it, and reuse the same profile on every deploy of that hardware class.

Profile shape:

system:
  features:
    hardware:
      profile:
        cpu: intel                    # intel | amd
        gpus: [intel, nvidia]         # any of: intel, amd, nvidia
        nvidia_supports_open: true    # set false to force proprietary/nouveau
        wireless: [intel]             # any of: intel, amd, atheros, broadcom,
                                      # mediatek, marvell, realtek, qcom, cirrus
        fingerprint: false            # set true to force fprintd install

The same keys (minus profile) can also be set directly under hardware as a declarative hardware group that MERGES over auto-detection (auto-detect = base; the group supplements/overrides it). Unlike profile, which skips detection entirely, the group keeps detection running and layers on top - use it to pin everything a known device needs so nothing is ever under-set.

Key Type Merge semantics
cpu str pin the CPU vendor (overrides detection when non-empty)
gpus/wireless/audio list union with the detected vendor codes
camera dict {uvc, ipu6} booleans OR'd with detection
fingerprint/bluetooth bool OR'd with detection (force-on)
packages dict per-os_family extra packages, added to the install set (deduped; empty entries dropped)
disable list feature/vendor names force-off, applied last
kernel_params list extra kernel cmdline params, appended to the bootloader

Example - a laptop with an Intel IPU6 camera (out-of-tree stack) and a Cirrus amp, pinned in a group's group_vars:

system:
  features:
    hardware:
      bluetooth: true                 # force-on if detection misses the combo card
      camera:
        ipu6: true                    # force the IPU6 path
      packages:                       # out-of-tree/AUR bits detection must not auto-install
        Archlinux: [intel-ipu6-dkms, v4l2-relayd, linux-firmware-cirrus]
      disable: [displaylink]          # never pull DisplayLink on this device
      kernel_params: ["i915.enable_psr=0"]

4.3 hypervisor Dictionary

Key Type Default Description
type string -- libvirt, proxmox, vmware, xen, or none
url string -- API host (Proxmox/VMware)
username string -- API username
password string -- API password
node string -- Target compute node (Proxmox node / VMware ESXi host; mutually exclusive with cluster on VMware)
storage string -- Storage identifier (Proxmox/VMware)
datacenter string -- VMware datacenter
cluster string -- VMware cluster
certs bool false TLS certificate validation (VMware)
ssh bool false Enable SSH on guest and switch connection (VMware)

4.4 CIS Hardening

When system.features.cis.enabled: true, the CIS role applies hardening. The behaviour is driven by three keys under system.features.cis:

Key Type Default Description
enabled bool false Apply CIS hardening at all
profile string default default (house baseline), l1 (clean CIS Level 1), or l2
rules dict {} Per-rule on/off overrides on top of the profile
params dict {} Parameter overrides (deep-merged; list values replace wholesale)

Profiles. default is the established house baseline (CIS Level 1 plus the USB lockdown, full module blacklist, and IPv6-disable extras, minus the usability-hostile controls). l1 is a clean CIS Level 1: it drops the L2 extras and adds password aging, AIDE, and warning banners. l2 is l1 plus auditd and the L2 extras.

Per-rule overrides. Toggle an individual rule without changing profile, e.g. keep the default profile but allow USB and IPv6 on a desktop:

system:
  features:
    cis:
      enabled: true
      rules:
        usb_lockdown: false
        ipv6_disable: false

Rule keys: module_blacklist, usb_lockdown, sysctl_hardening, ipv6_disable, umask_default, empty_password_login, pwquality, core_dumps, shell_timeout, journald_persistent, sudo_logfile, su_restriction, faillock, password_history, tcp_wrappers, crypto_policy, mask_services, cron_at_access, file_permissions, sshd_hardening, password_expiry, aide, warning_banners, auditd, and the opt-in grub_password (set rules.grub_password: true with params.grub_password_hash).

Parameters. Override baseline values under params (full list in roles/cis/vars/main.yml):

system:
  features:
    cis:
      enabled: true
      profile: l1
      params:
        pwquality_minlen: 16
        sysctl:                       # dict: deep-merged over the profile's set
          net.ipv4.ip_forward: 1
        sshd_options:                 # list: REPLACES the entire default list
          - {option: X11Forwarding, value: "yes"}

Common params: modules_blacklist (list), sysctl (dict), sshd_options (list), pwquality_minlen (14), tmout (900), umask (077), umask_profile (027), faillock_deny (5), faillock_unlock_time (900), password_remember (5), pass_max_days (365), aide_cron_hour/aide_cron_minute, banner_text, grub_password_hash.

4.5 VMware Guest Operations

When hypervisor.type: vmware uses the vmware_tools connection:

Variable Description
ansible_vmware_tools_user Guest OS username
ansible_vmware_tools_password Guest OS password
ansible_vmware_guest_path VM inventory path
ansible_vmware_host vCenter/ESXi hostname
ansible_vmware_user vCenter/ESXi API username
ansible_vmware_password vCenter/ESXi API password
ansible_vmware_validate_certs TLS certificate validation

4.6 Multi-Disk Schema

system.disks[0] is the OS disk (no mount.path). Additional entries define data disks.

Key Type Description
size number Disk size in GB (required for virtual)
device string Block device path (required for physical data disks)
partition string Derived from device during normalization (not user input)
mount.path string Mount point (additional disks only)
mount.fstype string btrfs, ext4, or xfs
mount.label string Filesystem label
mount.opts string Mount options (default: defaults)
system:
  disks:
    - size: 80                    # OS disk
    - size: 200                   # Data disk
      mount:
        path: /data
        fstype: xfs
        label: DATA

4.7 Advanced Partitioning Overrides

Variable Default Description
partitioning_efi_size_mib 512 EFI system partition size in MiB
partitioning_boot_size_mib 1024 Separate /boot size in MiB
partitioning_separate_boot auto-derived Force a separate /boot partition
partitioning_boot_fs_fstype auto-derived Filesystem for /boot
partitioning_use_full_disk true Use remaining VG space for root LV

Swap sizing: RAM >= 16GB gets swap = RAM/2. RAM < 16GB gets swap = max(RAM_GB, 2GB). Further capped to prevent over-allocation on small disks.

LVM layout (when not using btrfs): root, swap, and when CIS is enabled: /home (2-20GB, 10% of disk), /var (2GB), /var/log (2GB), /var/log/audit (1.5GB).

4.8 Cleanup Defaults

Post-install verification and recovery settings.

Variable Default Description
cleanup_verify_boot true Check VM accessibility after reboot
cleanup_boot_timeout 300 Timeout in seconds for boot verification
cleanup_remove_on_failure true Auto-remove VMs that fail to boot (created this run only)

5. Execution Pipeline

Roles execute in this order:

  1. global_defaults -- normalize inputs, validate, set OS flags
  2. system_check -- detect installer environment, verify live/non-prod target
  3. virtualization -- create VM (if virtual), attach disks, cloud-init
  4. environment -- prepare installer: mount ISO, configure repos, setup pacman, detect hardware
  5. partitioning -- create partitions, LVM, LUKS, mount filesystems
  6. bootstrap -- install base system, packages, and vendor-matched hardware bits
  7. configuration -- users, fstab, locales, bootloader, encryption enrollment, networking
  8. cis -- CIS hardening (when system.features.cis.enabled: true)
  9. cleanup -- unmount, shutdown installer, remove media, verify boot

6. Usage

ansible-playbook -i inventory.yml main.yml
ansible-playbook -i inventory.yml main.yml -e @vars.yml

All credentials (system.users, system.root.password) must be defined in inventory or passed via -e.

Example inventory files are included:

  • inventory_example.yml -- Proxmox virtual setup
  • inventory_libvirt_example.yml -- libvirt virtual setup
  • inventory_baremetal_example.yml -- bare-metal physical setup

7. Security

Use Ansible Vault for all sensitive values (hypervisor.password, system.luks.passphrase, user passwords in system.users, system.root.password).

8. Safety

The playbook aborts on non-live/production targets. It refuses to touch pre-existing VMs and only cleans up VMs created in the current run.