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
- Supported Platforms
- Compatibility Notes
- Configuration Model
- Variable Reference
- 4.1 Core Variables
- 4.2
systemDictionary - 4.3
hypervisorDictionary - 4.4
cisDictionary - 4.5 VMware Guest Operations
- 4.6 Multi-Disk Schema
- 4.7 Advanced Partitioning Overrides
- 4.8 Cleanup Defaults
- Execution Pipeline
- Usage
- Security
- 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_isois required forsystem.os: rhel.- RHEL installs should use
ext4orxfs(notbtrfs). custom_iso: trueskips ArchISO validation; your installer must provide required tooling.- On non-Arch installers, set
system.features.chroot.toolexplicitly.
3. Configuration Model
Two dict-based variables drive the entire configuration:
system-- host, network, users, disk layout, encryption, and feature toggleshypervisor-- virtualization backend credentials and targeting
An optional third dict cis overrides CIS hardening parameters when system.features.cis.enabled: true.
All three 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/cis.
| 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) |
mirror |
string | per-distro default | Override package mirror (Debian/Ubuntu) |
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.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) |
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) |
rhel_repo.source |
string | iso |
RHEL post-install repo source: iso, satellite, or none |
rhel_repo.url |
string | -- | Satellite/custom repo URL when source: satellite |
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 tonouveau. 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 Dictionary
When system.features.cis.enabled: true, the CIS role applies hardening. All values have sensible defaults; override specific keys via the cis dict.
| Key | Type | Default | Description |
|---|---|---|---|
modules_blacklist |
list | see below | Kernel modules to blacklist via modprobe |
sysctl |
dict | see below | Sysctl key/value pairs written to 10-cis.conf |
sshd_options |
list | see below | SSHD options applied via lineinfile |
pwquality_minlen |
int | 14 |
Minimum password length |
tmout |
int | 900 |
Shell timeout (seconds) |
umask |
string | 077 |
Default umask in bashrc |
umask_profile |
string | 027 |
Default umask in /etc/profile |
faillock_deny |
int | 5 |
Failed login attempts before lockout |
faillock_unlock_time |
int | 900 |
Lockout duration (seconds) |
password_remember |
int | 5 |
Password history depth |
Default modules blacklist: freevxfs, jffs2, hfs, hfsplus, cramfs, udf, usb-storage, dccp, sctp, rds, tipc, firewire-core, firewire-sbp2, thunderbolt. squashfs is added automatically except on Ubuntu (snap dependency).
Default sysctl settings include: kernel.yama.ptrace_scope=2, kernel.kptr_restrict=2, kernel.perf_event_paranoid=3, kernel.unprivileged_bpf_disabled=1, IPv4/IPv6 hardening, ARP protection, and IPv6 disabled by default. Override individual keys:
cis:
sysctl:
net.ipv6.conf.all.disable_ipv6: 0 # re-enable IPv6
net.ipv4.ip_forward: 1 # enable for routers/containers
Default SSHD options enforce: PermitRootLogin no, PasswordAuthentication no, X11Forwarding no, AllowTcpForwarding no, MaxAuthTries 4, and post-quantum KEX (mlkem768x25519-sha256 on OpenSSH 9.9+). Override per-option:
cis:
sshd_options:
- { option: X11Forwarding, value: "yes" }
- { option: AllowTcpForwarding, value: "yes" }
Note: providing sshd_options replaces the entire list. Copy the defaults from roles/cis/defaults/main.yml and modify as needed.
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:
- global_defaults -- normalize inputs, validate, set OS flags
- system_check -- detect installer environment, verify live/non-prod target
- virtualization -- create VM (if virtual), attach disks, cloud-init
- environment -- prepare installer: mount ISO, configure repos, setup pacman, detect hardware
- partitioning -- create partitions, LVM, LUKS, mount filesystems
- bootstrap -- install base system, packages, and vendor-matched hardware bits
- configuration -- users, fstab, locales, bootloader, encryption enrollment, networking
- cis -- CIS hardening (when
system.features.cis.enabled: true) - 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 setupinventory_libvirt_example.yml-- libvirt virtual setupinventory_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.