Compare commits
155 Commits
master
...
4336d864b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4336d864b3 | |||
| 62e50c19ff | |||
| b7cf1b10a9 | |||
| fc2d924349 | |||
| 67e3753ece | |||
| da9e287e56 | |||
| a8ea4f0962 | |||
| 754668b734 | |||
| 5b3076d9e1 | |||
| 1f778a7aaa | |||
| 54ffe5ff91 | |||
| 335534176f | |||
| 79227b4391 | |||
| 13faf33296 | |||
| de451be77b | |||
| af9f264cd3 | |||
| 750a085e19 | |||
| f2eb9f2c8e | |||
| 79988619c6 | |||
| fb69c96e4a | |||
| d586c087f8 | |||
| 9dd71b2559 | |||
| 35f1702447 | |||
| 8b18fbdb4c | |||
| 909a0a6021 | |||
| 2f3fce42b5 | |||
| b72816e985 | |||
| ac0b5caf83 | |||
| 3ddc3c72ed | |||
| f1af7ccbca | |||
| 51ca969ff4 | |||
| 1221249546 | |||
| 87fd69b825 | |||
| 3deb3ea751 | |||
| cc30637f09 | |||
| 23721aac96 | |||
| 5a9b346d72 | |||
| 75267e5140 | |||
| f0fb68992d | |||
| 0e3edb41f7 | |||
| 2bf0cb901e | |||
| 1216c79619 | |||
| 4efd64664d | |||
| dc5aa5077e | |||
| c65934c290 | |||
| 5b8438ac3b | |||
| 45df803131 | |||
| 30f74fa4bd | |||
| 19372db27e | |||
| d55fc5799d | |||
| 98231be0bd | |||
| c46a4a5a0a | |||
| b84688f1d6 | |||
| b1d2294d63 | |||
| ac339b54c4 | |||
| cb46a6989f | |||
| 73ea7a177b | |||
| 0f8faf0a22 | |||
| b520126253 | |||
| a4ca4c4ff4 | |||
| d9efb54bec | |||
| e7a0cc4f62 | |||
| a76f317f8f | |||
| e5bd152fb3 | |||
| 6d1c3577df | |||
| 86f0284acb | |||
| 221bb4d517 | |||
| e81ba76446 | |||
| 54bbb9d15c | |||
| f94b220020 | |||
| 3fd470d63e | |||
| a3cd507b2a | |||
| f74ec325ea | |||
| bef15af69f | |||
| 7970d933e8 | |||
| a123a32feb | |||
| 54c704de4e | |||
| 9308d09d7b | |||
| f367844239 | |||
| 53e4499d2b | |||
| eb63a4fa83 | |||
| 9e3688ae2b | |||
| dea01cc8a0 | |||
| 92c9702e1d | |||
| c837a52a24 | |||
| fbd57e0603 | |||
| 40a9ee9882 | |||
| 3448e95e5c | |||
| 074831833f | |||
| d1a5217e88 | |||
| 07492b5b57 | |||
| 14913bcd3d | |||
| 041650c287 | |||
| a63ffbc731 | |||
| 9d2f1cc5bd | |||
| f72f9feb9a | |||
| 417737f904 | |||
| a06c2ebdcf | |||
| e174ecda42 | |||
| 5246a905bb | |||
| d00d84b69c | |||
| 4dafa8c596 | |||
| 53584b8730 | |||
| ce40468b77 | |||
| 4b4fab3c33 | |||
| db2fab5e7d | |||
| 42be0a5919 | |||
| 17400fa6ff | |||
| deb14d2c94 | |||
| 65c5b1029b | |||
| a1fbb7c21d | |||
| d076ac8fef | |||
| c82e4afc4d | |||
| ac72fdc4a6 | |||
| b2e050c467 | |||
| 914d7dd9d1 | |||
| 21bf8f79e2 | |||
| 38feff4369 | |||
| 404529e8a4 | |||
| 3db18858c3 | |||
| 72a9576abe | |||
| 462c2c7dfe | |||
| ef8bfeaf84 | |||
| ba6be037ac | |||
| 5ca1c7f570 | |||
| cd8e477534 | |||
| c439e9741e | |||
| 0a5c70e49f | |||
| 19f2c9efe2 | |||
| 230c74fd9b | |||
| a2c19e2e49 | |||
| 9f9a4b38b8 | |||
| 524356cf8d | |||
| a2993212ca | |||
| fba2e5fc94 | |||
| cf68a93b45 | |||
| 3000268a0e | |||
| 196c5be67a | |||
| 33bad193b4 | |||
| d5277802f7 | |||
| 28e6cf50d1 | |||
| 42cb5071c2 | |||
| 23a798a63a | |||
| 5dd84c6b39 | |||
| d0ae20911b | |||
| b6d06dd96d | |||
| 09b3ed44ba | |||
| 603abe63cb | |||
| 1c0e6533ae | |||
| 00aa614cfd | |||
| 4905d10bc0 | |||
| b4e8ccb77f | |||
| 2a82ee4d5c | |||
| 7b213e7456 | |||
| cfc261878a |
330
README.md
330
README.md
@@ -13,7 +13,7 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
- 4.1 [Core Variables](#41-core-variables)
|
||||
- 4.2 [`system` Dictionary](#42-system-dictionary)
|
||||
- 4.3 [`hypervisor` Dictionary](#43-hypervisor-dictionary)
|
||||
- 4.4 [CIS Hardening](#44-cis-hardening)
|
||||
- 4.4 [`cis` Dictionary](#44-cis-dictionary)
|
||||
- 4.5 [VMware Guest Operations](#45-vmware-guest-operations)
|
||||
- 4.6 [Multi-Disk Schema](#46-multi-disk-schema)
|
||||
- 4.7 [Advanced Partitioning Overrides](#47-advanced-partitioning-overrides)
|
||||
@@ -29,14 +29,17 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
|
||||
| `system.os` | Distribution | `system.version` |
|
||||
| ------------ | ------------------------ | ------------------------------------- |
|
||||
| `almalinux` | AlmaLinux | `9`, `10` |
|
||||
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
|
||||
| `alpine` | Alpine Linux | latest (rolling) |
|
||||
| `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`) |
|
||||
| `debian` | Debian | `10`-`13`, `unstable` |
|
||||
| `fedora` | Fedora | `38`-`45` |
|
||||
| `opensuse` | openSUSE Tumbleweed | latest (rolling) |
|
||||
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
|
||||
| `rocky` | Rocky Linux | `8`, `9`, `10` |
|
||||
| `ubuntu` | Ubuntu (latest non-LTS) | optional (e.g. `24.04`) |
|
||||
| `ubuntu-lts` | Ubuntu LTS | optional (e.g. `24.04`) |
|
||||
| `void` | Void Linux | latest (rolling) |
|
||||
|
||||
### Hypervisors
|
||||
|
||||
@@ -59,10 +62,12 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
|
||||
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`)
|
||||
- **`system`** -- host, network, users, disk layout, encryption, and feature toggles
|
||||
- **`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.
|
||||
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
|
||||
|
||||
@@ -117,7 +122,7 @@ all:
|
||||
path: /data
|
||||
fstype: xfs
|
||||
users:
|
||||
ops:
|
||||
- name: ops
|
||||
password: !vault |
|
||||
$ANSIBLE_VAULT...
|
||||
keys:
|
||||
@@ -146,7 +151,7 @@ all:
|
||||
|
||||
### 4.1 Core Variables
|
||||
|
||||
Top-level variables outside `system`/`hypervisor`.
|
||||
Top-level variables outside `system`/`hypervisor`/`cis`.
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
| ---------------- | ------ | -------------------------- | ---------------------------------------------------- |
|
||||
@@ -162,7 +167,7 @@ Top-level variables outside `system`/`hypervisor`.
|
||||
| `type` | string | `virtual` | `virtual` or `physical` |
|
||||
| `os` | string | -- | Target distribution (see [table](#distributions)) |
|
||||
| `version` | string | -- | Version selector for versioned distros |
|
||||
| `filesystem` | string | `ext4` | `btrfs`, `ext4`, or `xfs` |
|
||||
| `filesystem` | string | -- | `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 |
|
||||
@@ -171,35 +176,15 @@ Top-level variables outside `system`/`hypervisor`.
|
||||
| `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) |
|
||||
| `path` | string | -- | Hypervisor folder/path |
|
||||
| `packages` | list | `[]` | Additional packages installed post-reboot |
|
||||
| `network` | dict | see below | Network configuration |
|
||||
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#46-multi-disk-schema)) |
|
||||
| `users` | dict | `{}` | User accounts (keyed by username) |
|
||||
| `users` | list | `[]` | User accounts |
|
||||
| `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 |
|
||||
@@ -217,36 +202,20 @@ When `interfaces` is empty, the flat fields (`bridge`, `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).
|
||||
|
||||
```yaml
|
||||
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) |
|
||||
| `name` | string | -- | Username (required) |
|
||||
| `password` | string | -- | User password (required for first 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`.
|
||||
The first user's credentials are prompted interactively via `vars_prompt` unless supplied in inventory or `-e`.
|
||||
|
||||
#### `system.root`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ---------- | ------ | ----------- | ------------- |
|
||||
| `password` | string | -- | Root password |
|
||||
| `shell` | string | `/bin/bash` | Login shell |
|
||||
| Key | Type | Default | Description |
|
||||
| ---------- | ------ | ------- | ------------- |
|
||||
| `password` | string | -- | Root password |
|
||||
|
||||
#### `system.luks`
|
||||
|
||||
@@ -265,30 +234,21 @@ Users must be defined in inventory. The dict format enables additive merging acr
|
||||
| `iter` | int | `4000` | PBKDF iteration time (ms) |
|
||||
| `bits` | int | `512` | Key size (bits) |
|
||||
| `pbkdf` | string | `argon2id` | PBKDF algorithm |
|
||||
| `urandom` | bool | `true` | Use urandom during key generation |
|
||||
| `verify` | bool | `true` | Verify passphrase during format |
|
||||
|
||||
#### `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`.
|
||||
| `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`) |
|
||||
|
||||
#### `system.features`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------ | ------ | -------------- | ------------------------------------ |
|
||||
| `cis.enabled` | bool | `false` | Enable CIS hardening (see [4.4](#44-cis-hardening)) |
|
||||
| `cis.profile` | string | `default` | CIS profile: `default`, `l1`, or `l2` (see [4.4](#44-cis-hardening)) |
|
||||
| `cis.rules` | dict | `{}` | Per-rule CIS overrides |
|
||||
| `cis.params` | dict | `{}` | CIS parameter overrides |
|
||||
| `cis.enabled` | bool | `false` | Enable CIS hardening (see [4.4](#44-cis-dictionary)) |
|
||||
| `selinux.enabled` | bool | `true` | SELinux management |
|
||||
| `firewall.enabled` | bool | `true` | Firewall setup |
|
||||
| `firewall.backend` | string | `firewalld` | `firewalld` or `ufw` |
|
||||
@@ -299,166 +259,6 @@ The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `
|
||||
| `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](#425-systemfeaturesdesktop)) |
|
||||
| `firmware.*` | dict | see below | Vendor firmware blobs and CPU microcode (see [4.2.6](#426-systemfeaturesfirmware)) |
|
||||
| `gpu.*` | dict | see below | Mesa/Vulkan and per-vendor GPU userspace (see [4.2.7](#427-systemfeaturesgpu)) |
|
||||
| `peripherals.*` | dict | see below | Fingerprint, camera, audio, bluetooth, DisplayLink (see [4.2.8](#428-systemfeaturesperipherals)) |
|
||||
| `hardware.*` | dict | see below | Hardware-detection profile override (see [4.2.9](#429-systemfeatureshardware)) |
|
||||
|
||||
**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:
|
||||
|
||||
```yaml
|
||||
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`:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
@@ -472,53 +272,47 @@ system:
|
||||
| `storage` | string | -- | Storage identifier (Proxmox/VMware) |
|
||||
| `datacenter` | string | -- | VMware datacenter |
|
||||
| `cluster` | string | -- | VMware cluster |
|
||||
| `certs` | bool | `false` | TLS certificate validation (VMware) |
|
||||
| `certs` | bool | `true` | TLS certificate validation (VMware) |
|
||||
| `ssh` | bool | `false` | Enable SSH on guest and switch connection (VMware) |
|
||||
|
||||
### 4.4 CIS Hardening
|
||||
### 4.4 `cis` Dictionary
|
||||
|
||||
When `system.features.cis.enabled: true`, the CIS role applies hardening. The behaviour is driven by three keys under `system.features.cis`:
|
||||
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 |
|
||||
| --------- | ------ | ----------- | ----------------------------------------------------------------- |
|
||||
| `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) |
|
||||
| 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 |
|
||||
|
||||
**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.
|
||||
**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).
|
||||
|
||||
**Per-rule overrides.** Toggle an individual rule without changing profile, e.g. keep the default profile but allow USB and IPv6 on a desktop:
|
||||
**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:
|
||||
|
||||
```yaml
|
||||
system:
|
||||
features:
|
||||
cis:
|
||||
enabled: true
|
||||
rules:
|
||||
usb_lockdown: false
|
||||
ipv6_disable: false
|
||||
cis:
|
||||
sysctl:
|
||||
net.ipv6.conf.all.disable_ipv6: 0 # re-enable IPv6
|
||||
net.ipv4.ip_forward: 1 # enable for routers/containers
|
||||
```
|
||||
|
||||
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`):
|
||||
**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:
|
||||
|
||||
```yaml
|
||||
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"}
|
||||
cis:
|
||||
sshd_options:
|
||||
- { option: X11Forwarding, value: "yes" }
|
||||
- { option: AllowTcpForwarding, 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`.
|
||||
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
|
||||
|
||||
@@ -542,7 +336,7 @@ When `hypervisor.type: vmware` uses the `vmware_tools` connection:
|
||||
| ------------- | ------ | ------------------------------------------------------ |
|
||||
| `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) |
|
||||
| `partition` | string | Partition device path (required for physical data disks) |
|
||||
| `mount.path` | string | Mount point (additional disks only) |
|
||||
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
|
||||
| `mount.label` | string | Filesystem label |
|
||||
@@ -590,9 +384,9 @@ 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
|
||||
4. **environment** -- prepare installer: mount ISO, configure repos, setup pacman
|
||||
5. **partitioning** -- create partitions, LVM, LUKS, mount filesystems
|
||||
6. **bootstrap** -- install base system, packages, and vendor-matched hardware bits
|
||||
6. **bootstrap** -- install base system and packages (OS-specific)
|
||||
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
|
||||
@@ -604,7 +398,7 @@ 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`.
|
||||
Credentials for the first user and root are prompted interactively via `vars_prompt` unless already set in inventory or passed via `-e`.
|
||||
|
||||
Example inventory files are included:
|
||||
|
||||
@@ -614,7 +408,7 @@ Example inventory files are included:
|
||||
|
||||
## 7. Security
|
||||
|
||||
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, user passwords in `system.users`, `system.root.password`).
|
||||
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, `system.users[].password`, `system.root.password`).
|
||||
|
||||
## 8. Safety
|
||||
|
||||
|
||||
@@ -3,6 +3,3 @@ hash_behaviour = merge
|
||||
interpreter_python = auto_silent
|
||||
deprecation_warnings = False
|
||||
host_key_checking = False
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args = -C -o ControlMaster=auto -o ControlPersist=600s -o ServerAliveInterval=30 -o ServerAliveCountMax=10
|
||||
|
||||
@@ -9,11 +9,8 @@ all:
|
||||
baremetal01.example.com:
|
||||
ansible_host: 10.0.0.162
|
||||
ansible_user: root
|
||||
ansible_password: "CHANGE_ME"
|
||||
ansible_become_password: "CHANGE_ME"
|
||||
# Required for physical installs: confirms the operator accepts that
|
||||
# install_drive will be wiped. system_check refuses to run without it.
|
||||
physical_install_confirmed: true
|
||||
ansible_password: "1234"
|
||||
ansible_become_password: "1234"
|
||||
system:
|
||||
type: "physical"
|
||||
os: "archlinux"
|
||||
@@ -21,10 +18,3 @@ all:
|
||||
disks:
|
||||
- device: "/dev/sda"
|
||||
size: 120
|
||||
users:
|
||||
admin:
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
root:
|
||||
password: "CHANGE_ME"
|
||||
|
||||
@@ -43,7 +43,7 @@ all:
|
||||
label: DATA
|
||||
opts: defaults
|
||||
users:
|
||||
ops:
|
||||
- name: "ops"
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
@@ -100,7 +100,7 @@ all:
|
||||
path: /srv/data
|
||||
fstype: ext4
|
||||
users:
|
||||
dbadmin:
|
||||
- name: "dbadmin"
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
|
||||
@@ -6,6 +6,7 @@ all:
|
||||
url: "localhost"
|
||||
username: ""
|
||||
password: ""
|
||||
host: ""
|
||||
storage: "default"
|
||||
boot_iso: "/var/lib/libvirt/images/archlinux-x86_64.iso"
|
||||
children:
|
||||
@@ -39,7 +40,7 @@ all:
|
||||
path: /var/www
|
||||
fstype: xfs
|
||||
users:
|
||||
web:
|
||||
- name: "web"
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
@@ -81,7 +82,7 @@ all:
|
||||
path: /data
|
||||
fstype: ext4
|
||||
users:
|
||||
db:
|
||||
- name: "db"
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
@@ -122,7 +123,7 @@ all:
|
||||
path: /data
|
||||
fstype: btrfs
|
||||
users:
|
||||
compute:
|
||||
- name: "compute"
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
|
||||
159
main.yml
159
main.yml
@@ -1,10 +1,107 @@
|
||||
---
|
||||
# Bootstrap pipeline — role execution order:
|
||||
# 1. global_defaults — normalize + validate system/hypervisor/disk input
|
||||
# 2. system_check — pre-flight hardware/environment safety checks
|
||||
# 3. virtualization — create VM on hypervisor (libvirt/proxmox/vmware/xen)
|
||||
# 4. environment — detect live ISO, configure installer network, install tools
|
||||
# 5. partitioning — partition disk, create FS, LUKS, LVM, mount everything
|
||||
# 6. bootstrap — debootstrap/pacstrap/dnf install the target OS into /mnt
|
||||
# 7. configuration — users, network, encryption, fstab, bootloader, services
|
||||
# 8. cis — CIS hardening (optional, per system.features.cis.enabled)
|
||||
# 9. cleanup — unmount, remove cloud-init artifacts, reboot/shutdown
|
||||
- name: Create and configure VMs
|
||||
hosts: "{{ bootstrap_target | default('all') }}"
|
||||
strategy: free # noqa: run-once[play]
|
||||
gather_facts: false
|
||||
become: true
|
||||
vars_prompt:
|
||||
- name: user_name
|
||||
prompt: |
|
||||
What is your username?
|
||||
private: false
|
||||
|
||||
- name: user_public_key
|
||||
prompt: |
|
||||
What is your ssh key?
|
||||
private: false
|
||||
|
||||
- name: user_password
|
||||
prompt: |
|
||||
What is your password?
|
||||
confirm: true
|
||||
|
||||
- name: root_password
|
||||
prompt: |
|
||||
What is your root password?
|
||||
confirm: true
|
||||
pre_tasks:
|
||||
- name: Apply prompted authentication values to system input
|
||||
no_log: true
|
||||
vars:
|
||||
system_input: "{{ system | default({}) }}"
|
||||
system_users_input: "{{ system_input.users | default([]) }}"
|
||||
system_first_user: >-
|
||||
{{
|
||||
system_users_input[0]
|
||||
if (system_users_input is iterable and system_users_input is not string
|
||||
and system_users_input is not mapping and system_users_input | length > 0)
|
||||
else {}
|
||||
}}
|
||||
system_root_input: "{{ (system_input.root | default({})) if (system_input.root is mapping) else {} }}"
|
||||
prompt_user_name: "{{ user_name | default(system_user_name | default(''), true) | string }}"
|
||||
prompt_user_key: "{{ user_public_key | default(user_key | default(system_user_key | default(''), true), true) | string | trim }}"
|
||||
prompt_user_password: "{{ user_password | default(system_user_password | default(''), true) | string }}"
|
||||
prompt_root_password: "{{ root_password | default(system_root_password | default(''), true) | string }}"
|
||||
resolved_user:
|
||||
name: >-
|
||||
{{
|
||||
system_first_user.name | string
|
||||
if (system_first_user.name | default('') | string | length) > 0
|
||||
else prompt_user_name
|
||||
}}
|
||||
keys: >-
|
||||
{{
|
||||
system_first_user['keys']
|
||||
if (system_first_user['keys'] is defined
|
||||
and system_first_user['keys'] is iterable
|
||||
and system_first_user['keys'] is not string
|
||||
and system_first_user['keys'] | length > 0)
|
||||
else (
|
||||
[prompt_user_key]
|
||||
if (prompt_user_key | length > 0)
|
||||
else []
|
||||
)
|
||||
}}
|
||||
password: >-
|
||||
{{
|
||||
system_first_user.password | string
|
||||
if (system_first_user.password | default('') | string | length) > 0
|
||||
else prompt_user_password
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
system: >-
|
||||
{{
|
||||
system_input
|
||||
| combine(
|
||||
{
|
||||
'users': (
|
||||
[resolved_user]
|
||||
+ (system_users_input[1:]
|
||||
if (system_users_input is sequence
|
||||
and system_users_input is not string
|
||||
and system_users_input | length > 1)
|
||||
else [])
|
||||
),
|
||||
'root': {
|
||||
'password': (
|
||||
(system_root_input.password | default('') | string | length) > 0
|
||||
) | ternary(system_root_input.password | string, prompt_root_password)
|
||||
}
|
||||
},
|
||||
recursive=True
|
||||
)
|
||||
}}
|
||||
|
||||
- name: Load global defaults
|
||||
ansible.builtin.import_role:
|
||||
name: global_defaults
|
||||
@@ -52,12 +149,6 @@
|
||||
name: configuration
|
||||
public: true
|
||||
|
||||
# Past this point the OS is installed and configured; a CIS hardening or
|
||||
# cleanup failure must not delete an otherwise-good VM.
|
||||
- name: Mark base system complete
|
||||
ansible.builtin.set_fact:
|
||||
_bootstrap_base_complete: true
|
||||
|
||||
- name: Apply CIS hardening
|
||||
when: system_cfg.features.cis.enabled | bool
|
||||
ansible.builtin.include_role:
|
||||
@@ -69,18 +160,15 @@
|
||||
ansible.builtin.include_role:
|
||||
name: cleanup
|
||||
public: true
|
||||
vars:
|
||||
ansible_become: false
|
||||
|
||||
rescue:
|
||||
- name: Decide whether to delete the half-built VM
|
||||
ansible.builtin.set_fact:
|
||||
_delete_vm_on_rescue: >-
|
||||
{{ _vm_absent_before_bootstrap | default(false) | bool
|
||||
and virtualization_vm_created_in_run | default(false) | bool
|
||||
and system_cfg.type == "virtual"
|
||||
and not (_bootstrap_base_complete | default(false) | bool) }}
|
||||
|
||||
- name: Delete VM on bootstrap failure
|
||||
when: _delete_vm_on_rescue | bool
|
||||
when:
|
||||
- _vm_absent_before_bootstrap | default(false) | bool
|
||||
- virtualization_vm_created_in_run | default(false) | bool
|
||||
- system_cfg.type == "virtual"
|
||||
ansible.builtin.include_role:
|
||||
name: virtualization
|
||||
tasks_from: delete
|
||||
@@ -94,8 +182,9 @@
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Bootstrap failed for {{ hostname }}.
|
||||
{{ 'VM was deleted to allow clean retry.' if (_delete_vm_on_rescue | bool)
|
||||
else 'VM kept (base system installed or not created this run).' }}
|
||||
{{ 'VM was deleted to allow clean retry.'
|
||||
if (virtualization_vm_created_in_run | default(false))
|
||||
else 'VM was not created in this run (kept).' }}
|
||||
|
||||
post_tasks:
|
||||
- name: Set post-reboot connection flags
|
||||
@@ -119,27 +208,13 @@
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
no_log: true
|
||||
vars:
|
||||
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
|
||||
ansible.builtin.set_fact:
|
||||
ansible_connection: ssh
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_port: 22
|
||||
ansible_user: "{{ _primary.key }}"
|
||||
ansible_password: "{{ _primary.value.password }}"
|
||||
ansible_become_password: "{{ _primary.value.password }}"
|
||||
ansible_user: "{{ system_cfg.users[0].name }}"
|
||||
ansible_password: "{{ system_cfg.users[0].password }}"
|
||||
ansible_become_password: "{{ system_cfg.users[0].password }}"
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
- name: Wait for the rebooted host to accept SSH
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
ansible.builtin.wait_for_connection:
|
||||
delay: 5
|
||||
sleep: 5
|
||||
# 600s: a selinux-enabled first boot relabels the filesystem and reboots once more.
|
||||
timeout: 600
|
||||
|
||||
- name: Re-gather facts for target OS after reboot
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
@@ -149,22 +224,6 @@
|
||||
- min
|
||||
- pkg_mgr
|
||||
|
||||
- name: Register with the Satellite content source
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
- system_cfg.content.source == 'satellite'
|
||||
- system_cfg.os | lower in os_family_rhel
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/roles/configuration/tasks/satellite_register.yml"
|
||||
|
||||
- name: Activate the firewall on the rebooted host
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
- system_cfg.features.firewall.backend == 'ufw'
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/roles/configuration/tasks/firewall.yml"
|
||||
vars:
|
||||
firewall_phase: postreboot
|
||||
|
||||
- name: Install post-reboot packages
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
---
|
||||
# OS -> task file mapping for bootstrap dispatch.
|
||||
# OS → task file mapping for bootstrap dispatch.
|
||||
# Each key matches a supported `os` value; value is the task file to include.
|
||||
bootstrap_os_task_map:
|
||||
almalinux: _dnf_family.yml
|
||||
alpine: alpine.yml
|
||||
archlinux: archlinux.yml
|
||||
debian: debian.yml
|
||||
fedora: _dnf_family.yml
|
||||
opensuse: opensuse.yml
|
||||
rocky: _dnf_family.yml
|
||||
rhel: rhel.yml
|
||||
ubuntu: ubuntu.yml
|
||||
ubuntu-lts: ubuntu.yml
|
||||
void: void.yml
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
- name: Load desktop package definitions
|
||||
ansible.builtin.include_vars:
|
||||
file: desktop.yml
|
||||
|
||||
- name: Resolve desktop packages
|
||||
vars:
|
||||
_de: "{{ system_cfg.features.desktop.environment }}"
|
||||
_family_pkgs: "{{ bootstrap_desktop_packages[os_family] | default({}) }}"
|
||||
_de_config: "{{ _family_pkgs[_de] | default({}) }}"
|
||||
_base: "{{ bootstrap_desktop_base_packages[os_family] | default([]) }}"
|
||||
_dm: "{{ system_cfg.features.desktop.display_manager | default('') }}"
|
||||
_dm_override_pkg: "{{ (bootstrap_dm_override_packages[_dm] | default({}))[os_family] | default('') }}"
|
||||
_requested_groups: "{{ system_cfg.features.desktop.groups | default([]) }}"
|
||||
_group_pkgs: >-
|
||||
{{
|
||||
_requested_groups
|
||||
| select('in', desktop_package_groups)
|
||||
| map('extract', desktop_package_groups)
|
||||
| map(attribute=os_family, default=[])
|
||||
| list
|
||||
| sum(start=[])
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_desktop_groups: "{{ _de_config.groups | default([]) }}"
|
||||
_desktop_packages: >-
|
||||
{{
|
||||
((_de_config.packages | default([])) + _base + _group_pkgs + [_dm_override_pkg])
|
||||
| reject('equalto', '')
|
||||
| unique
|
||||
| list
|
||||
}}
|
||||
|
||||
- name: Validate desktop environment is supported
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.features.desktop.environment in (bootstrap_desktop_packages[os_family] | default({}))
|
||||
fail_msg: >-
|
||||
Desktop environment '{{ system_cfg.features.desktop.environment }}'
|
||||
is not defined for os_family '{{ os_family }}'.
|
||||
Supported: {{ (bootstrap_desktop_packages[os_family] | default({})).keys() | join(', ') }}
|
||||
quiet: true
|
||||
|
||||
- name: Install desktop package groups
|
||||
when: _desktop_groups | length > 0
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version_major }}
|
||||
--setopt=install_weak_deps=False group install -y {{ _desktop_groups | join(' ') }}
|
||||
register: _desktop_group_result
|
||||
changed_when: _desktop_group_result.rc == 0
|
||||
|
||||
- name: Install desktop packages
|
||||
when: _desktop_packages | length > 0
|
||||
vars:
|
||||
_install_commands:
|
||||
RedHat: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version_major }}
|
||||
--setopt=install_weak_deps=False install -y {{ _desktop_packages | join(' ') }}
|
||||
Debian: >-
|
||||
{{ chroot_command }} env DEBIAN_FRONTEND=noninteractive
|
||||
apt install -y --install-recommends {{ _desktop_packages | join(' ') }}
|
||||
Archlinux: >-
|
||||
pacstrap /mnt {{ _desktop_packages | join(' ') }}
|
||||
ansible.builtin.command: "{{ _install_commands[os_family] }}"
|
||||
register: _desktop_pkg_result
|
||||
changed_when: _desktop_pkg_result.rc == 0
|
||||
@@ -13,14 +13,11 @@
|
||||
block:
|
||||
- name: "Install base system for {{ os | capitalize }}"
|
||||
ansible.builtin.command: >-
|
||||
dnf --releasever={{ os_version_major }} --best {{ _dnf_repos }}
|
||||
dnf --releasever={{ os_version }} --best {{ _dnf_repos }}
|
||||
--installroot=/mnt --setopt=install_weak_deps=False
|
||||
groupinstall -y {{ _dnf_groups }}
|
||||
register: bootstrap_dnf_base_result
|
||||
changed_when: bootstrap_dnf_base_result.rc == 0
|
||||
failed_when:
|
||||
- bootstrap_dnf_base_result.rc != 0
|
||||
- "'scriptlet' not in bootstrap_dnf_base_result.stderr"
|
||||
|
||||
- name: Ensure chroot has DNS resolution
|
||||
ansible.builtin.file:
|
||||
@@ -31,7 +28,7 @@
|
||||
|
||||
- name: Install extra packages
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version_major }} --setopt=install_weak_deps=False
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
|
||||
install -y {{ _dnf_extra }}
|
||||
register: bootstrap_dnf_extra_result
|
||||
changed_when: bootstrap_dnf_extra_result.rc == 0
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
- name: Load hardware package definitions
|
||||
ansible.builtin.include_vars:
|
||||
file: hardware.yml
|
||||
|
||||
- name: Validate hardware support for current os_family
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- os_family in bootstrap_hardware_packages
|
||||
- hardware_profile_active is defined
|
||||
fail_msg: >-
|
||||
Hardware feature requested but no package map for os_family
|
||||
'{{ os_family }}'. Extend roles/bootstrap/vars/hardware.yml.
|
||||
quiet: true
|
||||
|
||||
# nvidia_driver: auto -> open (Turing+) -> proprietary (older, if family ships it)
|
||||
# -> nouveau (fallback). Explicit value falls back to nouveau when
|
||||
# the family lacks packages for it.
|
||||
- name: Resolve Nvidia driver flavor
|
||||
vars:
|
||||
_family: "{{ bootstrap_hardware_packages[os_family] }}"
|
||||
_user_driver: "{{ system_cfg.features.gpu.nvidia_driver | default('auto') }}"
|
||||
_has_nvidia: "{{ 'nvidia' in (hardware_profile_active.gpus | default([]) | difference(_hardware_profile_disable | default([]))) }}"
|
||||
_supports_open: "{{ hardware_profile_active.nvidia_supports_open | default(true) | bool }}"
|
||||
_open_pkgs: "{{ _family.gpu_nvidia.open | default([]) }}"
|
||||
_prop_pkgs: "{{ _family.gpu_nvidia.proprietary | default([]) }}"
|
||||
_auto_choice: >-
|
||||
{{
|
||||
('open' if _supports_open and _open_pkgs | length > 0
|
||||
else ('proprietary' if _prop_pkgs | length > 0
|
||||
else 'nouveau'))
|
||||
}}
|
||||
_user_choice: >-
|
||||
{{
|
||||
_auto_choice if _user_driver == 'auto'
|
||||
else (_user_driver
|
||||
if (_family.gpu_nvidia[_user_driver] | default([]) | length > 0)
|
||||
else 'nouveau')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_nvidia_driver_resolved: "{{ _user_choice if _has_nvidia else 'nouveau' }}"
|
||||
|
||||
# Fedora's akmod-nvidia* packages live in RPMFusion non-free, which is not
|
||||
# enabled out of the box; install the release RPM before the package step.
|
||||
- name: Enable RPMFusion non-free for Fedora Nvidia install
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- os == 'fedora'
|
||||
- system_cfg.features.gpu.enabled | bool
|
||||
- _nvidia_driver_resolved in ['open', 'proprietary']
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf install -y
|
||||
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-{{ os_version_major }}.noarch.rpm
|
||||
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{{ os_version_major }}.noarch.rpm
|
||||
register: _rpmfusion_result
|
||||
changed_when: _rpmfusion_result.rc == 0
|
||||
|
||||
- name: Resolve hardware package set
|
||||
ansible.builtin.include_tasks: _resolve_hardware_packages.yml
|
||||
|
||||
- name: Report hardware package selection
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Hardware install ({{ os_family }}):
|
||||
cpu={{ hardware_profile_active.cpu | default('-') }},
|
||||
gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }},
|
||||
nvidia_driver={{ _nvidia_driver_resolved }},
|
||||
wireless={{ hardware_profile_active.wireless | default([]) | join(',') | default('-', true) }},
|
||||
fingerprint={{ hardware_profile_active.fingerprint | default(false) }}
|
||||
-> {{ _hardware_packages | length }} package(s)
|
||||
|
||||
- name: Note Intel IPU6 camera (out-of-tree stack)
|
||||
when: hardware_profile_active.camera.ipu6 | default(false) | bool
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Intel IPU6 MIPI camera detected. Its driver stack (intel-ipu6 firmware,
|
||||
DKMS module, v4l2-relayd, libcamera) is out-of-tree/AUR and is NOT auto-
|
||||
installed. Pin the packages in a hardware group via
|
||||
system.features.hardware.packages[{{ os_family }}].
|
||||
|
||||
- name: Install hardware packages
|
||||
when: _hardware_packages | length > 0
|
||||
vars:
|
||||
_install_commands:
|
||||
RedHat: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version_major }}
|
||||
--setopt=install_weak_deps=False install -y {{ _hardware_packages | join(' ') }}
|
||||
Debian: >-
|
||||
{{ chroot_command }} apt install -y {{ _hardware_packages | join(' ') }}
|
||||
Archlinux: >-
|
||||
pacstrap /mnt {{ _hardware_packages | join(' ') }}
|
||||
ansible.builtin.command: "{{ _install_commands[os_family] }}"
|
||||
register: _hardware_install_result
|
||||
changed_when: _hardware_install_result.rc == 0
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
# Split out of _hardware.yml so fixtures can seed the inputs and assert the
|
||||
# resolved _hardware_packages list with no chroot/install.
|
||||
- name: Resolve hardware package set
|
||||
vars:
|
||||
_family: "{{ bootstrap_hardware_packages[os_family] }}"
|
||||
_disable: "{{ _hardware_profile_disable | default([]) | list }}"
|
||||
_profile_packages: "{{ (_hardware_profile_packages | default({}))[os_family] | default([]) | list }}"
|
||||
_cpu: "{{ hardware_profile_active.cpu | default('') | string }}"
|
||||
_gpus: "{{ hardware_profile_active.gpus | default([]) | difference(_disable) | list }}"
|
||||
_wifi: "{{ hardware_profile_active.wireless | default([]) | difference(_disable) | list }}"
|
||||
_fp_detected: "{{ hardware_profile_active.fingerprint | default(false) | bool }}"
|
||||
_audio: "{{ hardware_profile_active.audio | default([]) | difference(_disable) | list }}"
|
||||
_bt_detected: "{{ hardware_profile_active.bluetooth | default(false) | bool }}"
|
||||
_firmware_on: "{{ system_cfg.features.firmware.enabled | bool }}"
|
||||
_microcode_on: "{{ _firmware_on and (system_cfg.features.firmware.microcode | bool) }}"
|
||||
_gpu_on: "{{ system_cfg.features.gpu.enabled | bool }}"
|
||||
_peripherals_on: "{{ system_cfg.features.peripherals.enabled | bool }}"
|
||||
_camera_pref: "{{ system_cfg.features.peripherals.camera | default('auto') }}"
|
||||
_camera_uvc: "{{ hardware_profile_active.camera.uvc | default(false) | bool }}"
|
||||
_camera_ipu6: "{{ hardware_profile_active.camera.ipu6 | default(false) | bool }}"
|
||||
_fp_pref: "{{ system_cfg.features.peripherals.fingerprint | default('auto') }}"
|
||||
_audio_pref: "{{ system_cfg.features.peripherals.audio | default('auto') }}"
|
||||
_bt_pref: "{{ system_cfg.features.peripherals.bluetooth | default('auto') }}"
|
||||
_dl_on: "{{ (system_cfg.features.peripherals.displaylink | bool) and ('displaylink' not in _disable) }}"
|
||||
_camera_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('camera' not in _disable)
|
||||
and (_camera_pref == 'true' or (_camera_pref == 'auto' and (_camera_uvc or _camera_ipu6)))
|
||||
}}
|
||||
_fp_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('fingerprint' not in _disable)
|
||||
and (_fp_pref == 'true' or (_fp_pref == 'auto' and _fp_detected))
|
||||
}}
|
||||
_audio_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('audio' not in _disable)
|
||||
and (_audio_pref == 'true' or (_audio_pref == 'auto' and (_audio | length > 0)))
|
||||
}}
|
||||
_bt_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('bluetooth' not in _disable)
|
||||
and (_bt_pref == 'true' or (_bt_pref == 'auto' and _bt_detected))
|
||||
}}
|
||||
# Union of GPU/wireless/CPU vendors; CPU vendor is included so Intel-CPU
|
||||
# systems pull i915/iwlwifi firmware via the same vendor split.
|
||||
_cpu_vendor_list: "{{ ([_cpu] if (_cpu | length > 0 and _cpu not in _disable) else []) | list }}"
|
||||
_firmware_vendors: >-
|
||||
{{
|
||||
(_firmware_on | ternary(
|
||||
(_gpus + _wifi + _cpu_vendor_list)
|
||||
| reject('equalto', '') | unique | list,
|
||||
[]
|
||||
))
|
||||
}}
|
||||
_microcode_pkgs: >-
|
||||
{{
|
||||
((_microcode_on and _cpu | length > 0 and _cpu not in _disable) | ternary(
|
||||
_family.cpu_microcode[_cpu] | default([]),
|
||||
[]
|
||||
)) | list
|
||||
}}
|
||||
_firmware_pkgs: >-
|
||||
{{
|
||||
(_firmware_on | ternary(
|
||||
(_family.firmware_base | default([]) | list)
|
||||
+ (_firmware_vendors
|
||||
| map('extract', _family.firmware | default({}))
|
||||
| select('truthy')
|
||||
| list
|
||||
| sum(start=[])),
|
||||
[]
|
||||
)) | list
|
||||
}}
|
||||
_gpu_base_pkgs: "{{ (_gpu_on | ternary(_family.gpu_base | default([]), [])) | list }}"
|
||||
_gpu_vendor_pkgs: >-
|
||||
{{
|
||||
(_gpu_on | ternary(
|
||||
(_gpus | reject('equalto', 'nvidia') | list)
|
||||
| map('extract', _family.gpu | default({}))
|
||||
| select('truthy')
|
||||
| list
|
||||
| sum(start=[]),
|
||||
[]
|
||||
)) | list
|
||||
}}
|
||||
_gpu_nvidia_pkgs: >-
|
||||
{{
|
||||
((_gpu_on and ('nvidia' in _gpus)) | ternary(
|
||||
_family.gpu_nvidia[_nvidia_driver_resolved] | default([]),
|
||||
[]
|
||||
)) | list
|
||||
}}
|
||||
_camera_base_pkgs: >-
|
||||
{{
|
||||
(_camera_on | ternary(_family.camera_base | default([]), [])) | list
|
||||
}}
|
||||
_peripherals_fingerprint_pkgs: >-
|
||||
{{
|
||||
(_fp_on | ternary(_family.peripherals_fingerprint | default([]), [])) | list
|
||||
}}
|
||||
_peripherals_displaylink_pkgs: >-
|
||||
{{
|
||||
(_dl_on | ternary(_family.peripherals_displaylink | default([]), [])) | list
|
||||
}}
|
||||
_audio_base_pkgs: "{{ (_audio_on | ternary(_family.audio_base | default([]), [])) | list }}"
|
||||
_bluetooth_base_pkgs: "{{ (_bt_on | ternary(_family.bluetooth_base | default([]), [])) | list }}"
|
||||
ansible.builtin.set_fact:
|
||||
_hardware_packages: >-
|
||||
{{
|
||||
(_microcode_pkgs + _firmware_pkgs
|
||||
+ _gpu_base_pkgs + _gpu_vendor_pkgs + _gpu_nvidia_pkgs
|
||||
+ _audio_base_pkgs + _bluetooth_base_pkgs
|
||||
+ _camera_base_pkgs + _peripherals_fingerprint_pkgs
|
||||
+ _peripherals_displaylink_pkgs
|
||||
+ _profile_packages)
|
||||
| reject('equalto', '')
|
||||
| unique
|
||||
| list
|
||||
}}
|
||||
30
roles/bootstrap/tasks/alpine.yml
Normal file
30
roles/bootstrap/tasks/alpine.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
- name: Bootstrap Alpine Linux
|
||||
vars:
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_base_packages: "{{ _config.base | join(' ') }}"
|
||||
_extra_packages: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install Alpine Linux base
|
||||
ansible.builtin.command: >
|
||||
apk --root /mnt --no-cache add {{ _base_packages }}
|
||||
register: bootstrap_alpine_bootstrap_result
|
||||
changed_when: bootstrap_alpine_bootstrap_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
when: _extra_packages | trim | length > 0
|
||||
ansible.builtin.command: >
|
||||
apk --root /mnt add {{ _extra_packages }}
|
||||
register: bootstrap_alpine_extra_result
|
||||
changed_when: bootstrap_alpine_extra_result.rc == 0
|
||||
|
||||
- name: Install bootloader
|
||||
ansible.builtin.command: >
|
||||
apk --root /mnt add grub grub-efi efibootmgr
|
||||
register: bootstrap_alpine_bootloader_result
|
||||
changed_when: bootstrap_alpine_bootloader_result.rc == 0
|
||||
@@ -8,44 +8,7 @@
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
block:
|
||||
- name: Notify that mirror mode falls back to the public mirrorlist
|
||||
when:
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length == 0
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
content.source is 'mirror' but content.url is empty: keeping the live
|
||||
ISO public mirrorlist (refreshed by reflector). Set content.url to pin
|
||||
a specific mirror.
|
||||
|
||||
- name: Point pacman at the content mirror
|
||||
when: system_cfg.content.url | length > 0
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/pacman.d/mirrorlist
|
||||
content: "Server = {{ system_cfg.content.url }}/$repo/os/$arch\n"
|
||||
mode: "0644"
|
||||
|
||||
- name: Refresh Arch keyring in the live environment
|
||||
ansible.builtin.command: pacman -Sy --noconfirm archlinux-keyring
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
register: bootstrap_arch_keyring
|
||||
changed_when: bootstrap_arch_keyring.rc == 0
|
||||
|
||||
- name: Install Arch base system
|
||||
ansible.builtin.command: >-
|
||||
pacstrap /mnt {{ bootstrap_archlinux_packages | join(' ') }}
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
register: bootstrap_result
|
||||
changed_when: bootstrap_result.rc == 0
|
||||
|
||||
- name: Persist the content mirror in the installed system
|
||||
when: system_cfg.content.url | length > 0
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/pacman.d/mirrorlist
|
||||
content: "Server = {{ system_cfg.content.url }}/$repo/os/$arch\n"
|
||||
mode: "0644"
|
||||
ansible.builtin.command: >-
|
||||
pacstrap /mnt {{ bootstrap_archlinux_packages | join(' ') }}
|
||||
register: bootstrap_result
|
||||
changed_when: bootstrap_result.rc == 0
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
vars:
|
||||
bootstrap_debian_release: >-
|
||||
{{
|
||||
'bookworm' if (os_version | string) == '12'
|
||||
'buster' if (os_version | string) == '10'
|
||||
else 'bullseye' if (os_version | string) == '11'
|
||||
else 'bookworm' if (os_version | string) == '12'
|
||||
else 'trixie' if (os_version | string) == '13'
|
||||
else 'sid' if (os_version | string) == 'unstable'
|
||||
else 'trixie'
|
||||
@@ -26,27 +28,10 @@
|
||||
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
|
||||
quiet: true
|
||||
|
||||
- name: Check for a debootstrap script for the target release
|
||||
ansible.builtin.stat:
|
||||
path: "/usr/share/debootstrap/scripts/{{ bootstrap_debian_release }}"
|
||||
register: bootstrap_debian_script
|
||||
|
||||
- name: Symlink a missing debootstrap script to the sid base
|
||||
ansible.builtin.file:
|
||||
src: sid
|
||||
dest: "/usr/share/debootstrap/scripts/{{ bootstrap_debian_release }}"
|
||||
state: link
|
||||
when: not bootstrap_debian_script.stat.exists
|
||||
|
||||
- name: Install Debian base system
|
||||
ansible.builtin.command: >-
|
||||
debootstrap --keyring=/usr/share/keyrings/debian-archive-keyring.gpg
|
||||
--include={{ bootstrap_debian_base_csv }}
|
||||
{{ bootstrap_debian_release }} /mnt
|
||||
{{ system_cfg.content.url }}
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
debootstrap --include={{ bootstrap_debian_base_csv }}
|
||||
{{ bootstrap_debian_release }} /mnt {{ system_cfg.mirror }}
|
||||
register: bootstrap_debian_base_result
|
||||
changed_when: bootstrap_debian_base_result.rc == 0
|
||||
|
||||
@@ -63,10 +48,6 @@
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
Acquire::http::Proxy "{{ system_cfg.content.proxy }}";
|
||||
Acquire::https::Proxy "{{ system_cfg.content.proxy }}";
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Update package lists
|
||||
@@ -74,21 +55,13 @@
|
||||
register: bootstrap_debian_update_result
|
||||
changed_when: bootstrap_debian_update_result.rc == 0
|
||||
|
||||
- name: Upgrade all packages to latest versions
|
||||
ansible.builtin.command: "{{ chroot_command }} apt full-upgrade -y"
|
||||
register: bootstrap_debian_upgrade_result
|
||||
changed_when: "'0 upgraded' not in bootstrap_debian_upgrade_result.stdout"
|
||||
|
||||
- name: Install extra packages
|
||||
when: bootstrap_debian_extra_args | trim | length > 0
|
||||
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}"
|
||||
register: bootstrap_debian_extra_result
|
||||
changed_when: bootstrap_debian_extra_result.rc == 0
|
||||
|
||||
# Printing (libcups2) and mDNS (libavahi*) are needed by a desktop session,
|
||||
# so keep them when a desktop is requested.
|
||||
- name: Remove unnecessary packages
|
||||
when: not (system_cfg.features.desktop.enabled | bool)
|
||||
ansible.builtin.command: "{{ chroot_command }} apt remove -y libcups2 libavahi-common3 libavahi-common-data"
|
||||
register: bootstrap_debian_remove_result
|
||||
changed_when: bootstrap_debian_remove_result.rc == 0
|
||||
|
||||
@@ -29,46 +29,11 @@
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
# Installers write their cache inside the installroot; redirect it off the 2 GiB CIS /var LV.
|
||||
- name: Create bootstrap package-cache directory
|
||||
ansible.builtin.file:
|
||||
path: /mnt/.bootstrap-cache
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Redirect package cache off the CIS /var LV
|
||||
ansible.posix.mount:
|
||||
src: /mnt/.bootstrap-cache
|
||||
path: /mnt/var/cache
|
||||
fstype: none
|
||||
opts: bind
|
||||
state: ephemeral
|
||||
|
||||
- name: Run OS-specific bootstrap process
|
||||
vars:
|
||||
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
|
||||
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
|
||||
|
||||
# dnf --installroot never runs anaconda, so no authselect profile is selected and
|
||||
# /etc/pam.d/system-auth is missing, leaving the system unable to authenticate.
|
||||
# local is the right profile: local-auth only, no pam_sss.so, still CIS-capable.
|
||||
- name: Select default authselect profile for the PAM stack
|
||||
when: is_authselect | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} authselect select local --force"
|
||||
register: bootstrap_authselect_result
|
||||
changed_when: bootstrap_authselect_result.rc == 0
|
||||
|
||||
- name: Install hardware-matched firmware/microcode/GPU/peripheral packages
|
||||
when: >-
|
||||
(system_cfg.features.firmware.enabled | bool)
|
||||
or (system_cfg.features.gpu.enabled | bool)
|
||||
or (system_cfg.features.peripherals.enabled | bool)
|
||||
ansible.builtin.include_tasks: _hardware.yml
|
||||
|
||||
- name: Install desktop environment packages
|
||||
when: system_cfg.features.desktop.enabled | bool
|
||||
ansible.builtin.include_tasks: _desktop.yml
|
||||
|
||||
- name: Ensure chroot uses live environment DNS
|
||||
ansible.builtin.file:
|
||||
src: /run/NetworkManager/resolv.conf
|
||||
|
||||
30
roles/bootstrap/tasks/opensuse.yml
Normal file
30
roles/bootstrap/tasks/opensuse.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
- name: Bootstrap openSUSE
|
||||
vars:
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_base_patterns: "{{ _config.base | join(' ') }}"
|
||||
_extra_packages: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install openSUSE base patterns
|
||||
ansible.builtin.command: >
|
||||
zypper --root /mnt --non-interactive install -t pattern {{ _base_patterns }}
|
||||
register: bootstrap_opensuse_base_result
|
||||
changed_when: bootstrap_opensuse_base_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
when: _extra_packages | trim | length > 0
|
||||
ansible.builtin.command: >
|
||||
zypper --root /mnt --non-interactive install {{ _extra_packages }}
|
||||
register: bootstrap_opensuse_extra_result
|
||||
changed_when: bootstrap_opensuse_extra_result.rc == 0
|
||||
|
||||
- name: Install bootloader
|
||||
ansible.builtin.command: >
|
||||
zypper --root /mnt --non-interactive install grub2 grub2-efi efibootmgr
|
||||
register: bootstrap_opensuse_bootloader_result
|
||||
changed_when: bootstrap_opensuse_bootloader_result.rc == 0
|
||||
@@ -24,14 +24,12 @@
|
||||
- "'grub2-common' not in (bootstrap_result.stderr | default(''))"
|
||||
|
||||
- name: Ensure chroot RHEL DVD directory exists
|
||||
when: system_cfg.content.source != 'mirror'
|
||||
ansible.builtin.file:
|
||||
path: /mnt/usr/local/install/redhat/dvd
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Bind mount RHEL DVD into chroot
|
||||
when: system_cfg.content.source != 'mirror'
|
||||
ansible.posix.mount:
|
||||
src: /usr/local/install/redhat/dvd
|
||||
path: /mnt/usr/local/install/redhat/dvd
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# ubuntu = latest non-LTS, ubuntu-lts = latest LTS
|
||||
bootstrap_ubuntu_release_map:
|
||||
ubuntu: questing
|
||||
ubuntu-lts: resolute
|
||||
bootstrap_ubuntu_release: "{{ bootstrap_ubuntu_release_map[os] | default('resolute') }}"
|
||||
ubuntu-lts: noble
|
||||
bootstrap_ubuntu_release: "{{ bootstrap_ubuntu_release_map[os] | default('noble') }}"
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
bootstrap_ubuntu_base_csv: "{{ (['ca-certificates'] + _config.base) | unique | join(',') }}"
|
||||
bootstrap_ubuntu_extra_args: >-
|
||||
@@ -24,28 +24,13 @@
|
||||
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
|
||||
quiet: true
|
||||
|
||||
- name: Check for a debootstrap script for the target release
|
||||
ansible.builtin.stat:
|
||||
path: "/usr/share/debootstrap/scripts/{{ bootstrap_ubuntu_release }}"
|
||||
register: bootstrap_ubuntu_script
|
||||
|
||||
- name: Symlink a missing debootstrap script to the ubuntu base
|
||||
ansible.builtin.file:
|
||||
src: gutsy
|
||||
dest: "/usr/share/debootstrap/scripts/{{ bootstrap_ubuntu_release }}"
|
||||
state: link
|
||||
when: not bootstrap_ubuntu_script.stat.exists
|
||||
|
||||
- name: Install Ubuntu base system
|
||||
ansible.builtin.command: >-
|
||||
debootstrap
|
||||
--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
--include={{ bootstrap_ubuntu_base_csv }}
|
||||
{{ bootstrap_ubuntu_release }} /mnt
|
||||
{{ system_cfg.content.url }}
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
{{ system_cfg.mirror }}
|
||||
register: bootstrap_ubuntu_base_result
|
||||
changed_when: bootstrap_ubuntu_base_result.rc == 0
|
||||
|
||||
@@ -62,10 +47,6 @@
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
Acquire::http::Proxy "{{ system_cfg.content.proxy }}";
|
||||
Acquire::https::Proxy "{{ system_cfg.content.proxy }}";
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Update package lists
|
||||
@@ -73,11 +54,6 @@
|
||||
register: bootstrap_ubuntu_update_result
|
||||
changed_when: bootstrap_ubuntu_update_result.rc == 0
|
||||
|
||||
- name: Upgrade all packages to latest versions
|
||||
ansible.builtin.command: "{{ chroot_command }} apt full-upgrade -y"
|
||||
register: bootstrap_ubuntu_upgrade_result
|
||||
changed_when: "'0 upgraded' not in bootstrap_ubuntu_upgrade_result.stdout"
|
||||
|
||||
- name: Install extra packages
|
||||
when: bootstrap_ubuntu_extra_args | trim | length > 0
|
||||
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}"
|
||||
|
||||
30
roles/bootstrap/tasks/void.yml
Normal file
30
roles/bootstrap/tasks/void.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
- name: Bootstrap Void Linux
|
||||
vars:
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_base_packages: "{{ _config.base | join(' ') }}"
|
||||
_extra_packages: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install Void Linux base
|
||||
ansible.builtin.command: >
|
||||
xbps-install -Sy -r /mnt -R https://repo-default.voidlinux.org/current {{ _base_packages }}
|
||||
register: bootstrap_void_base_result
|
||||
changed_when: bootstrap_void_base_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
when: _extra_packages | trim | length > 0
|
||||
ansible.builtin.command: >
|
||||
xbps-install -Su -r /mnt {{ _extra_packages }}
|
||||
register: bootstrap_void_extra_result
|
||||
changed_when: bootstrap_void_extra_result.rc == 0
|
||||
|
||||
- name: Install bootloader
|
||||
ansible.builtin.command: >
|
||||
xbps-install -Sy -r /mnt grub-x86_64-efi efibootmgr
|
||||
register: bootstrap_void_bootloader_result
|
||||
changed_when: bootstrap_void_bootloader_result.rc == 0
|
||||
@@ -1,7 +1,7 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = bootstrap_debian_release %}
|
||||
{% set mirror = system_cfg.content.url | default('http://deb.debian.org/debian', true) %}
|
||||
{% set components = 'main contrib non-free non-free-firmware' %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = bootstrap_ubuntu_release %}
|
||||
{% set mirror = system_cfg.content.url %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main restricted universe multiverse' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
---
|
||||
# Wayland only: gnome, kde, sway, hyprland. No X11/xorg-server, no X11-only DEs.
|
||||
|
||||
# plasma-login-manager on Arch/Fedora44+ (Plasma 6.6), else sddm.
|
||||
bootstrap_kde_login_manager: >-
|
||||
{{
|
||||
'plasma-login-manager'
|
||||
if (os == 'archlinux' or (os == 'fedora' and (os_version | int) >= 44))
|
||||
else 'sddm'
|
||||
}}
|
||||
|
||||
# Native DMs ride in each DE's package set; only explicit non-native overrides
|
||||
# need a package here. ly is Arch-only (validation rejects it elsewhere first).
|
||||
bootstrap_dm_override_packages:
|
||||
ly:
|
||||
Archlinux: ly
|
||||
|
||||
# EL = non-fedora RedHat.
|
||||
bootstrap_os_is_el: "{{ os in ['almalinux', 'rocky', 'rhel'] }}"
|
||||
bootstrap_os_is_el10: "{{ bootstrap_os_is_el | bool and (os_version | default('0') | int) >= 10 }}"
|
||||
# EL10 renames (evince->papers, eog->loupe, ppd->tuned-ppd); fira-code + mpv absent on EL.
|
||||
bootstrap_desktop_browser: "{{ 'firefox-esr' if os == 'debian' else 'firefox' }}"
|
||||
bootstrap_desktop_pdf: "{{ 'papers' if bootstrap_os_is_el10 | bool else 'evince' }}"
|
||||
bootstrap_desktop_image: "{{ 'loupe' if bootstrap_os_is_el10 | bool else 'eog' }}"
|
||||
bootstrap_desktop_power: "{{ 'tuned-ppd' if bootstrap_os_is_el10 | bool else 'power-profiles-daemon' }}"
|
||||
bootstrap_desktop_redhat_codefont: "{{ '' if bootstrap_os_is_el | bool else 'fira-code-fonts' }}"
|
||||
bootstrap_desktop_redhat_video: "{{ '' if bootstrap_os_is_el | bool else 'mpv' }}"
|
||||
|
||||
bootstrap_desktop_packages:
|
||||
RedHat:
|
||||
gnome:
|
||||
groups: []
|
||||
packages:
|
||||
- gnome-shell
|
||||
- gnome-control-center
|
||||
- nautilus
|
||||
- gnome-session
|
||||
- gdm
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- plasma-systemmonitor
|
||||
- "{{ bootstrap_kde_login_manager }}"
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
- kscreen
|
||||
- kde-gtk-config
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
Debian:
|
||||
gnome:
|
||||
groups: []
|
||||
packages:
|
||||
- gnome-core
|
||||
- gdm3
|
||||
- gnome-tweaks
|
||||
- xdg-user-dirs
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- "{{ bootstrap_kde_login_manager }}"
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
- kscreen
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
Archlinux:
|
||||
gnome:
|
||||
groups: []
|
||||
packages:
|
||||
- gnome
|
||||
- gdm
|
||||
- xdg-user-dirs
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- "{{ bootstrap_kde_login_manager }}"
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
- kscreen
|
||||
- kde-gtk-config
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
sway:
|
||||
groups: []
|
||||
packages:
|
||||
- sway
|
||||
- waybar
|
||||
- foot
|
||||
- wofi
|
||||
- nautilus
|
||||
- greetd
|
||||
- greetd-tuigreet
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-wlr
|
||||
- polkit-gnome
|
||||
- bluez
|
||||
hyprland:
|
||||
groups: []
|
||||
packages:
|
||||
- hyprland
|
||||
- kitty
|
||||
- wofi
|
||||
- waybar
|
||||
- nautilus
|
||||
- greetd
|
||||
- greetd-tuigreet
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-hyprland
|
||||
- polkit-kde-agent
|
||||
- qt5-wayland
|
||||
- qt6-wayland
|
||||
- bluez
|
||||
|
||||
# Installed for EVERY DE whenever desktop.enabled. No file manager here: DE metas
|
||||
# bundle their own and the wlroots sets above carry nautilus.
|
||||
bootstrap_desktop_base_packages:
|
||||
RedHat:
|
||||
- google-noto-sans-fonts
|
||||
- google-noto-emoji-fonts
|
||||
- "{{ bootstrap_desktop_redhat_codefont }}"
|
||||
- pipewire
|
||||
- wireplumber
|
||||
- pipewire-pulseaudio
|
||||
- xdg-desktop-portal
|
||||
- "{{ bootstrap_desktop_power }}"
|
||||
- bluez
|
||||
- firefox
|
||||
- "{{ bootstrap_desktop_pdf }}"
|
||||
- "{{ bootstrap_desktop_image }}"
|
||||
- "{{ bootstrap_desktop_redhat_video }}"
|
||||
Debian:
|
||||
- fonts-noto
|
||||
- fonts-noto-color-emoji
|
||||
- fonts-firacode
|
||||
- pipewire
|
||||
- wireplumber
|
||||
- pipewire-pulse
|
||||
- xdg-desktop-portal
|
||||
- power-profiles-daemon
|
||||
- bluez
|
||||
- "{{ bootstrap_desktop_browser }}"
|
||||
- evince
|
||||
- eog
|
||||
- mpv
|
||||
Archlinux:
|
||||
- noto-fonts
|
||||
- noto-fonts-emoji
|
||||
- ttf-nerd-fonts-symbols
|
||||
- pipewire
|
||||
- wireplumber
|
||||
- pipewire-pulse
|
||||
- xdg-desktop-portal
|
||||
- power-profiles-daemon
|
||||
- bluez
|
||||
- firefox
|
||||
- evince
|
||||
- loupe
|
||||
- mpv
|
||||
|
||||
# Opt-in groups selected per host via features.desktop.groups; the union of the
|
||||
# requested groups' packages is installed. Empty selection by default.
|
||||
desktop_package_groups:
|
||||
dev:
|
||||
RedHat:
|
||||
- git
|
||||
- "@development-tools"
|
||||
- neovim
|
||||
- python3-pip
|
||||
Debian:
|
||||
- git
|
||||
- build-essential
|
||||
- neovim
|
||||
- python3-pip
|
||||
Archlinux:
|
||||
- git
|
||||
- base-devel
|
||||
- neovim
|
||||
- python-pip
|
||||
@@ -1,103 +0,0 @@
|
||||
---
|
||||
# Hardware-aware package definitions keyed by os_family, consumed by
|
||||
# _resolve_hardware_packages.yml. Only packages matching detected hardware are
|
||||
# installed; families without vendor splits collapse to one firmware meta package.
|
||||
bootstrap_hardware_packages:
|
||||
Archlinux:
|
||||
cpu_microcode:
|
||||
intel: [intel-ucode]
|
||||
amd: [amd-ucode]
|
||||
firmware_base: []
|
||||
firmware:
|
||||
intel: [linux-firmware-other] # iwlwifi + i915 firmware live here
|
||||
amd: [linux-firmware-amdgpu]
|
||||
nvidia: [linux-firmware-nvidia]
|
||||
atheros: [linux-firmware-atheros]
|
||||
broadcom: [linux-firmware-broadcom]
|
||||
mediatek: [linux-firmware-mediatek]
|
||||
marvell: [linux-firmware-marvell]
|
||||
realtek: [linux-firmware-realtek]
|
||||
qcom: [linux-firmware-qcom]
|
||||
cirrus: [linux-firmware-cirrus]
|
||||
other: [linux-firmware-other]
|
||||
gpu_base: [mesa, vulkan-icd-loader]
|
||||
gpu:
|
||||
intel: [vulkan-intel, intel-media-driver]
|
||||
amd: [vulkan-radeon, libva-mesa-driver]
|
||||
gpu_nvidia:
|
||||
open: [nvidia-open-dkms, nvidia-utils]
|
||||
proprietary: [nvidia-dkms, nvidia-utils]
|
||||
# Wayland-only: kernel nouveau module + mesa/gbm drive the display; no Xorg DDX.
|
||||
nouveau: [vulkan-nouveau]
|
||||
camera_base: [v4l-utils]
|
||||
peripherals_fingerprint: [fprintd, libfprint]
|
||||
peripherals_displaylink: [] # AUR only; user must wire in AUR helper
|
||||
audio_base: [sof-firmware, alsa-ucm-conf]
|
||||
bluetooth_base: [bluez, bluez-utils]
|
||||
|
||||
Debian:
|
||||
cpu_microcode:
|
||||
intel: [intel-microcode]
|
||||
amd: [amd64-microcode]
|
||||
firmware_base: [firmware-linux-free]
|
||||
firmware:
|
||||
intel: [firmware-iwlwifi, firmware-misc-nonfree]
|
||||
amd: [firmware-amd-graphics, firmware-misc-nonfree]
|
||||
nvidia: [firmware-misc-nonfree]
|
||||
atheros: [firmware-atheros]
|
||||
broadcom: [firmware-brcm80211]
|
||||
mediatek: [firmware-misc-nonfree]
|
||||
marvell: [firmware-misc-nonfree]
|
||||
realtek: [firmware-realtek]
|
||||
qcom: [firmware-misc-nonfree]
|
||||
cirrus: [firmware-misc-nonfree]
|
||||
other: [firmware-misc-nonfree]
|
||||
gpu_base: [mesa-vulkan-drivers, libgl1-mesa-dri]
|
||||
gpu:
|
||||
intel: [intel-media-va-driver, i965-va-driver]
|
||||
amd: [libva-glx2, mesa-va-drivers]
|
||||
gpu_nvidia:
|
||||
# Debian trixie+ ships nvidia-open-kernel-dkms; older releases only have
|
||||
# the proprietary nvidia-driver. Both come from the non-free component.
|
||||
open: [nvidia-open-kernel-dkms, nvidia-driver, nvidia-vulkan-icd]
|
||||
proprietary: [nvidia-driver, nvidia-vulkan-icd]
|
||||
# Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
|
||||
nouveau: []
|
||||
camera_base: [v4l-utils]
|
||||
peripherals_fingerprint: [fprintd, libpam-fprintd]
|
||||
peripherals_displaylink: [evdi-dkms] # userspace driver still needs vendor .run
|
||||
audio_base: [firmware-sof-signed, alsa-ucm-conf]
|
||||
bluetooth_base: [bluez]
|
||||
|
||||
RedHat:
|
||||
cpu_microcode:
|
||||
intel: [microcode_ctl]
|
||||
amd: [microcode_ctl]
|
||||
firmware_base: [linux-firmware]
|
||||
firmware:
|
||||
intel: []
|
||||
amd: []
|
||||
nvidia: []
|
||||
atheros: []
|
||||
broadcom: []
|
||||
mediatek: []
|
||||
marvell: []
|
||||
realtek: []
|
||||
qcom: []
|
||||
cirrus: []
|
||||
other: []
|
||||
gpu_base: [mesa-dri-drivers, mesa-vulkan-drivers, vulkan-loader]
|
||||
gpu:
|
||||
intel: [intel-media-driver, libva-intel-driver]
|
||||
amd: [mesa-va-drivers]
|
||||
gpu_nvidia:
|
||||
# akmod packages from RPMFusion non-free; repo enabled by _hardware.yml.
|
||||
open: [akmod-nvidia-open, xorg-x11-drv-nvidia, xorg-x11-drv-nvidia-cuda]
|
||||
proprietary: [akmod-nvidia, xorg-x11-drv-nvidia, xorg-x11-drv-nvidia-cuda]
|
||||
# Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
|
||||
nouveau: []
|
||||
camera_base: [v4l-utils]
|
||||
peripherals_fingerprint: [fprintd, fprintd-pam]
|
||||
peripherals_displaylink: [evdi] # COPR-supplied; repo enablement deferred
|
||||
audio_base: [alsa-sof-firmware, alsa-ucm]
|
||||
bluetooth_base: [bluez]
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# Feature-gated packages shared across all distros. Arch strips nftables from
|
||||
# this and composes it differently.
|
||||
# Feature-gated packages shared across all distros.
|
||||
# Arch has special nftables handling and composes this differently.
|
||||
bootstrap_common_conditional: >-
|
||||
{{
|
||||
(
|
||||
@@ -11,37 +11,18 @@ bootstrap_common_conditional: >-
|
||||
+ (['cryptsetup', 'tpm2-tools'] if system_cfg.luks.enabled | bool else [])
|
||||
+ (['qemu-guest-agent'] if hypervisor_type in ['libvirt', 'proxmox'] else [])
|
||||
+ (['open-vm-tools'] if hypervisor_type == 'vmware' else [])
|
||||
+ (['cloud-init'] if system_cfg.features.cloud_init | bool else [])
|
||||
)
|
||||
}}
|
||||
|
||||
# Native-installer parity backfill: anaconda and the d-i "standard" task leave
|
||||
# these, but install_weak_deps=False / Recommends-off minimal installs drop them.
|
||||
bootstrap_el_runtime:
|
||||
- NetworkManager
|
||||
- authselect
|
||||
- authselect-libs
|
||||
- chrony
|
||||
- crypto-policies
|
||||
- crypto-policies-scripts
|
||||
- dbus
|
||||
- polkit
|
||||
|
||||
bootstrap_deb_runtime:
|
||||
- apparmor-utils
|
||||
- chrony
|
||||
- libpam-pwquality
|
||||
- needrestart
|
||||
- network-manager
|
||||
- sudo
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-OS package definitions: base (rootfs/group install), extra (post-base),
|
||||
# conditional (feature/version-gated, appended by task files). DNF distros also
|
||||
# carry repos and use base as group names.
|
||||
# conditional (feature/version-gated, appended by task files).
|
||||
# DNF-based distros also carry repos (dnf --repo) and use base as group names.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
bootstrap_rhel:
|
||||
repos:
|
||||
- "rhel{{ os_version_major }}-baseos"
|
||||
- "rhel{{ os_version_major }}-appstream"
|
||||
base:
|
||||
- core
|
||||
- base
|
||||
@@ -70,7 +51,6 @@ bootstrap_rhel:
|
||||
+ (['python39'] if os_version_major | default('') == '8' else ['python'])
|
||||
+ (['kernel'] if os_version_major | default('') == '10' else [])
|
||||
+ (['zram-generator'] if os_version_major | default('') in ['9', '10'] else [])
|
||||
+ bootstrap_el_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -105,8 +85,8 @@ bootstrap_almalinux:
|
||||
- zstd
|
||||
conditional: >-
|
||||
{{
|
||||
(['dhcp-client'] if (os_version_major | default('10') | int) < 10 else [])
|
||||
+ bootstrap_el_runtime
|
||||
(['dbus-daemon'] if (os_version_major | default('10') | int) >= 9 else [])
|
||||
+ (['dhcp-client'] if (os_version_major | default('10') | int) < 10 else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -145,7 +125,6 @@ bootstrap_rocky:
|
||||
conditional: >-
|
||||
{{
|
||||
(['dhcp-client'] if (os_version_major | default('9') | int) < 10 else [])
|
||||
+ bootstrap_el_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -179,6 +158,7 @@ bootstrap_fedora:
|
||||
- nc
|
||||
- nfs-utils
|
||||
- nfsv4-client-utils
|
||||
- polkit
|
||||
- ppp
|
||||
- python3
|
||||
- ripgrep
|
||||
@@ -189,7 +169,7 @@ bootstrap_fedora:
|
||||
- zoxide
|
||||
- zram-generator
|
||||
- zstd
|
||||
conditional: "{{ bootstrap_el_runtime + bootstrap_common_conditional }}"
|
||||
conditional: "{{ bootstrap_common_conditional }}"
|
||||
|
||||
bootstrap_debian:
|
||||
base:
|
||||
@@ -207,22 +187,27 @@ bootstrap_debian:
|
||||
- python3
|
||||
- xfsprogs
|
||||
extra:
|
||||
- apparmor-utils
|
||||
- bat
|
||||
- chrony
|
||||
- curl
|
||||
- entr
|
||||
- fish
|
||||
- fzf
|
||||
- htop
|
||||
- jq
|
||||
- libpam-pwquality
|
||||
- linux-image-amd64
|
||||
- lrzsz
|
||||
- mtr
|
||||
- ncdu
|
||||
- net-tools
|
||||
- network-manager
|
||||
- python-is-python3
|
||||
- ripgrep
|
||||
- rsync
|
||||
- screen
|
||||
- sudo
|
||||
- syslog-ng
|
||||
- tcpd
|
||||
- vim
|
||||
@@ -236,8 +221,6 @@ bootstrap_debian:
|
||||
+ (['software-properties-common'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||
+ (['systemd-zram-generator'] if (os_version | string) not in ['10', '11'] else [])
|
||||
+ (['tldr'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
+ bootstrap_deb_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -259,8 +242,10 @@ bootstrap_ubuntu:
|
||||
- python3
|
||||
- xfsprogs
|
||||
extra:
|
||||
- apparmor-utils
|
||||
- bash-completion
|
||||
- bat
|
||||
- chrony
|
||||
- curl
|
||||
- dnsutils
|
||||
- duf
|
||||
@@ -272,16 +257,19 @@ bootstrap_ubuntu:
|
||||
- fzf
|
||||
- htop
|
||||
- jq
|
||||
- libpam-pwquality
|
||||
- lrzsz
|
||||
- mtr
|
||||
- ncdu
|
||||
- ncurses-term
|
||||
- net-tools
|
||||
- network-manager
|
||||
- python-is-python3
|
||||
- ripgrep
|
||||
- rsync
|
||||
- screen
|
||||
- software-properties-common
|
||||
- sudo
|
||||
- syslog-ng
|
||||
- systemd-zram-generator
|
||||
- tcpd
|
||||
@@ -294,8 +282,7 @@ bootstrap_ubuntu:
|
||||
- zstd
|
||||
conditional: >-
|
||||
{{
|
||||
(['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
+ bootstrap_deb_runtime
|
||||
(['tldr'] if (os_version | default('') | string | length) > 0 else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -322,6 +309,7 @@ bootstrap_archlinux:
|
||||
- nfs-utils
|
||||
- ppp
|
||||
- python
|
||||
- reflector
|
||||
- rsync
|
||||
- sudo
|
||||
- tldr
|
||||
@@ -333,7 +321,75 @@ bootstrap_archlinux:
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ (['iptables-nft'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ (['sbctl'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
+ (['reflector'] if system_cfg.content.url | length == 0 else [])
|
||||
+ (bootstrap_common_conditional | reject('equalto', 'nftables') | list)
|
||||
}}
|
||||
|
||||
bootstrap_alpine:
|
||||
base:
|
||||
- alpine-base
|
||||
extra:
|
||||
- btrfs-progs
|
||||
- chrony
|
||||
- curl
|
||||
- e2fsprogs
|
||||
- linux-lts
|
||||
- logrotate
|
||||
- lvm2
|
||||
- python3
|
||||
- rsync
|
||||
- sudo
|
||||
- util-linux
|
||||
- vim
|
||||
- xfsprogs
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_opensuse:
|
||||
base:
|
||||
- patterns-base-base
|
||||
extra:
|
||||
- btrfs-progs
|
||||
- chrony
|
||||
- curl
|
||||
- e2fsprogs
|
||||
- glibc-locale
|
||||
- kernel-default
|
||||
- logrotate
|
||||
- lvm2
|
||||
- NetworkManager
|
||||
- python3
|
||||
- rsync
|
||||
- sudo
|
||||
- vim
|
||||
- xfsprogs
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_void:
|
||||
base:
|
||||
- base-system
|
||||
- void-repo-nonfree
|
||||
extra:
|
||||
- btrfs-progs
|
||||
- chrony
|
||||
- curl
|
||||
- dhcpcd
|
||||
- e2fsprogs
|
||||
- logrotate
|
||||
- lvm2
|
||||
- python3
|
||||
- rsync
|
||||
- sudo
|
||||
- vim
|
||||
- xfsprogs
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,100 @@
|
||||
---
|
||||
# User-facing API: override via top-level `cis` dict in inventory.
|
||||
# Merged with these defaults in _normalize.yml → cis_cfg.
|
||||
cis_defaults:
|
||||
modules_blacklist:
|
||||
- freevxfs
|
||||
- jffs2
|
||||
- hfs
|
||||
- hfsplus
|
||||
- cramfs
|
||||
- udf
|
||||
- usb-storage
|
||||
- dccp
|
||||
- sctp
|
||||
- rds
|
||||
- tipc
|
||||
- firewire-core
|
||||
- firewire-sbp2
|
||||
- thunderbolt
|
||||
sysctl:
|
||||
fs.suid_dumpable: 0
|
||||
kernel.dmesg_restrict: 1
|
||||
kernel.kptr_restrict: 2
|
||||
kernel.perf_event_paranoid: 3
|
||||
kernel.unprivileged_bpf_disabled: 1
|
||||
kernel.yama.ptrace_scope: 2
|
||||
kernel.randomize_va_space: 2
|
||||
net.ipv4.ip_forward: 0
|
||||
net.ipv4.tcp_syncookies: 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts: 1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses: 1
|
||||
net.ipv4.conf.all.log_martians: 1
|
||||
net.ipv4.conf.all.rp_filter: 1
|
||||
net.ipv4.conf.all.secure_redirects: 0
|
||||
net.ipv4.conf.all.send_redirects: 0
|
||||
net.ipv4.conf.all.accept_redirects: 0
|
||||
net.ipv4.conf.all.accept_source_route: 0
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
net.ipv4.conf.default.log_martians: 1
|
||||
net.ipv4.conf.default.rp_filter: 1
|
||||
net.ipv4.conf.default.secure_redirects: 0
|
||||
net.ipv4.conf.default.send_redirects: 0
|
||||
net.ipv4.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.all.accept_redirects: 0
|
||||
net.ipv6.conf.all.disable_ipv6: 1
|
||||
net.ipv6.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.default.disable_ipv6: 1
|
||||
net.ipv6.conf.lo.disable_ipv6: 1
|
||||
sshd_options:
|
||||
- { option: LogLevel, value: VERBOSE }
|
||||
- { option: LoginGraceTime, value: "60" }
|
||||
- { option: PermitRootLogin, value: "no" }
|
||||
- { option: StrictModes, value: "yes" }
|
||||
- { option: MaxAuthTries, value: "4" }
|
||||
- { option: MaxSessions, value: "10" }
|
||||
- { option: MaxStartups, value: "10:30:60" }
|
||||
- { option: PubkeyAuthentication, value: "yes" }
|
||||
- { option: HostbasedAuthentication, value: "no" }
|
||||
- { option: IgnoreRhosts, value: "yes" }
|
||||
- { option: PasswordAuthentication, value: "no" }
|
||||
- { option: PermitEmptyPasswords, value: "no" }
|
||||
- { option: KerberosAuthentication, value: "no" }
|
||||
- { option: GSSAPIAuthentication, value: "no" }
|
||||
- { option: AllowAgentForwarding, value: "no" }
|
||||
- { option: AllowTcpForwarding, value: "no" }
|
||||
- { option: KbdInteractiveAuthentication, value: "no" }
|
||||
- { option: GatewayPorts, value: "no" }
|
||||
- { option: X11Forwarding, value: "no" }
|
||||
- { option: PermitUserEnvironment, value: "no" }
|
||||
- { option: ClientAliveInterval, value: "300" }
|
||||
- { option: ClientAliveCountMax, value: "1" }
|
||||
- { option: PermitTunnel, value: "no" }
|
||||
- { option: Banner, value: /etc/issue.net }
|
||||
pwquality_minlen: 14
|
||||
tmout: 900
|
||||
umask: "077"
|
||||
umask_profile: "027"
|
||||
faillock_deny: 5
|
||||
faillock_unlock_time: 900
|
||||
password_remember: 5
|
||||
|
||||
# Platform-specific binary names for CIS permission targets
|
||||
cis_fusermount_binary: "{{ 'fusermount3' if is_rhel | default(false) | bool else 'fusermount' }}"
|
||||
cis_write_binary: "{{ 'write' if is_rhel | default(false) | bool else 'wall' }}"
|
||||
|
||||
cis: {}
|
||||
|
||||
cis_permission_targets:
|
||||
- {path: "/mnt/etc/ssh/sshd_config", mode: "0600"}
|
||||
- {path: "/mnt/etc/cron.hourly", mode: "0700"}
|
||||
- {path: "/mnt/etc/cron.daily", mode: "0700"}
|
||||
- {path: "/mnt/etc/cron.weekly", mode: "0700"}
|
||||
- {path: "/mnt/etc/cron.monthly", mode: "0700"}
|
||||
- {path: "/mnt/etc/cron.d", mode: "0700"}
|
||||
- {path: "/mnt/etc/crontab", mode: "0600"}
|
||||
- {path: "/mnt/etc/logrotate.conf", mode: "0644"}
|
||||
- {path: "/mnt/usr/sbin/pppd", mode: "0754"}
|
||||
- {path: "/mnt/usr/bin/{{ cis_fusermount_binary }}", mode: "0755"}
|
||||
- {path: "/mnt/usr/bin/{{ cis_write_binary }}", mode: "0755"}
|
||||
- { path: "/mnt/etc/ssh/sshd_config", mode: "0600" }
|
||||
- { path: "/mnt/etc/cron.hourly", mode: "0700" }
|
||||
- { path: "/mnt/etc/cron.daily", mode: "0700" }
|
||||
- { path: "/mnt/etc/cron.weekly", mode: "0700" }
|
||||
- { path: "/mnt/etc/cron.monthly", mode: "0700" }
|
||||
- { path: "/mnt/etc/cron.d", mode: "0700" }
|
||||
- { path: "/mnt/etc/crontab", mode: "0600" }
|
||||
- { path: "/mnt/etc/logrotate.conf", mode: "0644" }
|
||||
- { path: "/mnt/usr/sbin/pppd", mode: "0754" }
|
||||
- { path: "/mnt/usr/bin/{{ cis_fusermount_binary }}", mode: "0755" }
|
||||
- { path: "/mnt/usr/bin/{{ cis_write_binary }}", mode: "0755" }
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
---
|
||||
- name: Determine CIS profile
|
||||
- name: Normalize CIS input
|
||||
ansible.builtin.set_fact:
|
||||
cis_profile: "{{ system_cfg.features.cis.profile | default('default') }}"
|
||||
cis_enabled: "{{ cis is defined and (cis is mapping or cis | bool) }}"
|
||||
cis_input: "{{ cis if cis is mapping else {} }}"
|
||||
|
||||
- name: Validate CIS profile selection
|
||||
ansible.builtin.assert:
|
||||
that: cis_profile in cis_profiles
|
||||
fail_msg: >-
|
||||
system.features.cis.profile '{{ cis_profile }}' is unknown
|
||||
(valid: {{ cis_profiles.keys() | list | join(', ') }}).
|
||||
quiet: true
|
||||
|
||||
- name: Resolve CIS rules and parameters
|
||||
vars:
|
||||
_cis: "{{ system_cfg.features.cis | default({}) }}"
|
||||
- name: Normalize CIS configuration
|
||||
when: cis_enabled and cis_cfg is not defined
|
||||
ansible.builtin.set_fact:
|
||||
cis_effective_rules: "{{ cis_profiles[cis_profile] | combine(_cis.rules | default({})) }}"
|
||||
cis_cfg: >-
|
||||
{{ cis_param_defaults
|
||||
| combine(cis_profile_params[cis_profile] | default({}), recursive=True)
|
||||
| combine(_cis.params | default({}), recursive=True) }}
|
||||
# l1/l2 add the stricter CIS-server controls on top of the legacy `default`
|
||||
# baseline; gate those tasks on this so `default` stays byte-for-byte unchanged.
|
||||
cis_strict: "{{ cis_profile in ['l1', 'l2'] }}"
|
||||
cis_cfg: "{{ cis_defaults | combine(cis_input, recursive=True) }}"
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
- name: Install AIDE
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
# Debian's aideinit lives in aide-common (only Recommended, so absent under
|
||||
# the installer's --no-install-recommends); pull it explicitly.
|
||||
ansible.builtin.command: "{{ cis_pkg_install }} {{ 'aide aide-common' if is_debian | bool else 'aide' }}"
|
||||
register: cis_aide_install
|
||||
changed_when: cis_aide_install.rc == 0
|
||||
|
||||
- name: Initialize the AIDE database
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
# Absolute path: arch-chroot's PATH omits /usr/sbin, so bare aide/aideinit is rc127.
|
||||
# Debian's aideinit assembles its split config; RHEL/Arch run --init on /etc/aide.conf.
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ '/usr/sbin/aideinit -y -f' if is_debian | bool else '/usr/sbin/aide --init' }}"
|
||||
register: cis_aide_init
|
||||
changed_when: cis_aide_init.rc == 0
|
||||
|
||||
- name: Locate the freshly built AIDE database
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/var/lib/aide
|
||||
patterns: "aide.db.new*"
|
||||
register: cis_aide_newdb
|
||||
|
||||
- name: Activate the AIDE database
|
||||
when:
|
||||
- cis_effective_rules.aide | default(false)
|
||||
- cis_aide_newdb.files | length > 0
|
||||
ansible.builtin.copy:
|
||||
src: "{{ cis_aide_newdb.files[0].path }}"
|
||||
dest: "{{ cis_aide_newdb.files[0].path | regex_replace('\\.new', '') }}"
|
||||
remote_src: true
|
||||
mode: "0600"
|
||||
|
||||
- name: Schedule the daily AIDE integrity check
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/cron.d/cis-aide
|
||||
mode: "0644"
|
||||
content: |
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
{{ cis_cfg.aide_cron_minute }} {{ cis_cfg.aide_cron_hour }} * * * root aide --check
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
- name: Install the audit daemon
|
||||
when: cis_effective_rules.auditd | default(false)
|
||||
ansible.builtin.command: "{{ cis_pkg_install }} {{ 'auditd' if is_debian | bool else 'audit' }}"
|
||||
register: cis_auditd_install
|
||||
changed_when: cis_auditd_install.rc == 0
|
||||
|
||||
- name: Deploy the CIS audit rule set
|
||||
when: cis_effective_rules.auditd | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/audit/rules.d/cis.rules
|
||||
mode: "0640"
|
||||
content: |
|
||||
## CIS baseline audit rules
|
||||
-D
|
||||
-b 8192
|
||||
-f 1
|
||||
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
|
||||
-w /etc/localtime -p wa -k time-change
|
||||
-w /etc/group -p wa -k identity
|
||||
-w /etc/passwd -p wa -k identity
|
||||
-w /etc/shadow -p wa -k identity
|
||||
-w /etc/gshadow -p wa -k identity
|
||||
-w /etc/security/opasswd -p wa -k identity
|
||||
-a always,exit -F arch=b64 -S sethostname,setdomainname -k system-locale
|
||||
-w /etc/hosts -p wa -k system-locale
|
||||
-w /var/log/lastlog -p wa -k logins
|
||||
-w /var/run/faillock -p wa -k logins
|
||||
-w /var/run/utmp -p wa -k session
|
||||
-w /var/log/wtmp -p wa -k session
|
||||
-w /var/log/btmp -p wa -k session
|
||||
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat,chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
-w /etc/sudoers -p wa -k scope
|
||||
-w /etc/sudoers.d -p wa -k scope
|
||||
-a always,exit -F arch=b64 -S init_module,delete_module -k modules
|
||||
-e 2
|
||||
|
||||
- name: Enable the audit daemon
|
||||
when: cis_effective_rules.auditd | default(false)
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable auditd"
|
||||
register: cis_auditd_enable
|
||||
changed_when: "'Created symlink' in cis_auditd_enable.stderr"
|
||||
@@ -1,35 +1,12 @@
|
||||
---
|
||||
- name: Ensure the Default UMASK is Set Correctly
|
||||
when: cis_effective_rules.umask_default | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/mnt/etc/profile"
|
||||
regexp: "^(\\s*)umask\\s+\\d+"
|
||||
line: "umask {{ cis_cfg.umask_profile }}"
|
||||
|
||||
- name: Set the login.defs UMASK (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.umask_default | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/login.defs
|
||||
regexp: '^\s*#?\s*UMASK\b'
|
||||
line: "UMASK\t\t{{ cis_cfg.umask_profile }}"
|
||||
|
||||
# authselect regenerates system-auth from the profile, so a direct edit is lost
|
||||
# on the next apply; without-nullok is the supported way to drop nullok there.
|
||||
- name: Prevent Login to Accounts With Empty Password (authselect)
|
||||
when:
|
||||
- cis_effective_rules.empty_password_login | default(false)
|
||||
- is_authselect | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} authselect enable-feature without-nullok"
|
||||
register: cis_nullok_result
|
||||
changed_when: cis_nullok_result.rc == 0
|
||||
|
||||
# Non-RHEL/non-Debian distros: loop evaluates to [] (intentional skip)
|
||||
- name: Prevent Login to Accounts With Empty Password
|
||||
when:
|
||||
- cis_effective_rules.empty_password_login | default(false)
|
||||
- not is_authselect | bool
|
||||
ansible.builtin.replace:
|
||||
dest: "{{ item }}"
|
||||
regexp: "\\s*nullok"
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
---
|
||||
# Fedora ships its own crypto-policies preset and update-crypto-policies
|
||||
# behaves differently; applying DEFAULT:NO-SHA1 can break package signing.
|
||||
# EL10 dropped the NO-SHA1 subpolicy module (DEFAULT already disables SHA-1
|
||||
# signatures), so the modifier is set only on EL9 and below.
|
||||
- name: Configure System Cryptography Policy
|
||||
vars:
|
||||
_cis_crypto_policy: "{{ 'DEFAULT' if (os_version_major | int >= 10) else 'DEFAULT:NO-SHA1' }}"
|
||||
when:
|
||||
- cis_effective_rules.crypto_policy | default(false)
|
||||
- os in (os_family_rhel | difference(['fedora']))
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set {{ _cis_crypto_policy }}"
|
||||
when: os in (os_family_rhel | difference(['fedora']))
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1"
|
||||
register: cis_crypto_policy_result
|
||||
changed_when: "'Setting system-wide crypto-policies to' in cis_crypto_policy_result.stdout"
|
||||
|
||||
- name: Mask Systemd Services
|
||||
when: cis_effective_rules.mask_services | default(false)
|
||||
ansible.builtin.command: >
|
||||
{{ chroot_command }} systemctl mask {{ 'nftables' if system_cfg.features.firewall.toolkit == 'iptables' else 'iptables' }} bluetooth rpcbind
|
||||
register: cis_mask_services_result
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
- name: Ensure cron and at access files exist
|
||||
when: cis_effective_rules.cron_at_access | default(false)
|
||||
- name: Ensure files exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: touch
|
||||
@@ -8,19 +7,10 @@
|
||||
loop:
|
||||
- /mnt/etc/at.allow
|
||||
- /mnt/etc/cron.allow
|
||||
|
||||
- name: Ensure TCP wrapper files exist
|
||||
when: cis_effective_rules.tcp_wrappers | default(false)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: touch
|
||||
mode: "0600"
|
||||
loop:
|
||||
- /mnt/etc/hosts.allow
|
||||
- /mnt/etc/hosts.deny
|
||||
|
||||
- name: Ensure cron and at deny files do not exist
|
||||
when: cis_effective_rules.cron_at_access | default(false)
|
||||
- name: Ensure files do not exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
# Opt-in only: a GRUB superuser password blocks unattended menu edits; the default entry still boots.
|
||||
- name: Assert a GRUB password hash is supplied
|
||||
when: cis_effective_rules.grub_password | default(false)
|
||||
ansible.builtin.assert:
|
||||
that: cis_cfg.grub_password_hash | length > 0
|
||||
fail_msg: >-
|
||||
system.features.cis.rules.grub_password is enabled but
|
||||
system.features.cis.params.grub_password_hash is empty. Generate one with
|
||||
grub2-mkpasswd-pbkdf2 and set it there.
|
||||
quiet: true
|
||||
|
||||
- name: Deploy the GRUB superuser password
|
||||
when: cis_effective_rules.grub_password | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/grub.d/01_cis_password
|
||||
mode: "0755"
|
||||
content: |
|
||||
#!/bin/sh
|
||||
cat <<'EOF'
|
||||
set superusers="root"
|
||||
password_pbkdf2 root {{ cis_cfg.grub_password_hash }}
|
||||
EOF
|
||||
|
||||
- name: Regenerate the GRUB configuration
|
||||
when: cis_effective_rules.grub_password | default(false)
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }}
|
||||
{{ 'grub2-mkconfig -o /boot/grub2/grub.cfg' if is_rhel | bool else 'grub-mkconfig -o /boot/grub/grub.cfg' }}
|
||||
register: cis_grub_regen
|
||||
changed_when: cis_grub_regen.rc == 0
|
||||
@@ -3,6 +3,7 @@
|
||||
ansible.builtin.import_tasks: _normalize.yml
|
||||
|
||||
- name: Apply CIS hardening
|
||||
when: cis_enabled
|
||||
block:
|
||||
- name: Include CIS hardening tasks
|
||||
ansible.builtin.include_tasks: "{{ cis_task }}"
|
||||
@@ -15,11 +16,5 @@
|
||||
- security_lines.yml
|
||||
- permissions.yml
|
||||
- sshd.yml
|
||||
- warning_banners.yml
|
||||
- password_expiry.yml
|
||||
- aide.yml
|
||||
- auditd.yml
|
||||
- packages.yml
|
||||
- grub_password.yml
|
||||
loop_control:
|
||||
loop_var: cis_task
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
- name: Disable Kernel Modules
|
||||
when: cis_effective_rules.module_blacklist | default(false)
|
||||
vars:
|
||||
# Ubuntu uses squashfs for snap packages - blacklisting it breaks snap entirely
|
||||
# Ubuntu uses squashfs for snap packages — blacklisting it breaks snap entirely
|
||||
cis_modules_squashfs: "{{ [] if os in ['ubuntu', 'ubuntu-lts'] else ['squashfs'] }}"
|
||||
cis_modules_all: "{{ cis_cfg.modules_blacklist + cis_modules_squashfs }}"
|
||||
ansible.builtin.copy:
|
||||
@@ -15,13 +14,11 @@
|
||||
{% endfor %}
|
||||
|
||||
- name: Remove old USB rules file
|
||||
when: cis_effective_rules.usb_lockdown | default(false)
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/udev/rules.d/10-cis_usb_devices.sh
|
||||
state: absent
|
||||
|
||||
- name: Create USB rules
|
||||
when: cis_effective_rules.usb_lockdown | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/udev/rules.d/10-cis_usb_devices.rules
|
||||
mode: "0644"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
# CIS L1 names legacy cleartext clients (telnet) for removal. They are absent on
|
||||
# a fresh minimal install; query first and remove only when present so the run
|
||||
# stays idempotent (a chroot package-manager remove cannot use the package module).
|
||||
- name: Check for insecure cleartext clients
|
||||
when: cis_strict | default(false)
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }}
|
||||
{{ 'dpkg -s' if is_debian | bool else 'pacman -Q' if os == 'archlinux' else 'rpm -q' }}
|
||||
{{ item }}
|
||||
loop: "{{ cis_cfg.insecure_packages }}"
|
||||
register: cis_insecure_present
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Remove insecure cleartext clients (CIS L1+)
|
||||
when:
|
||||
- cis_strict | default(false)
|
||||
- item.rc == 0
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }}
|
||||
{{ 'apt-get remove -y' if is_debian | bool else 'pacman -R --noconfirm' if os == 'archlinux' else 'dnf remove -y' }}
|
||||
{{ item.item }}
|
||||
loop: "{{ cis_insecure_present.results | default([]) }}"
|
||||
changed_when: true
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
# login.defs sets policy for future accounts; existing service accounts are intentionally not chage-aged.
|
||||
- name: Configure password aging defaults
|
||||
when: cis_effective_rules.password_expiry | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/login.defs
|
||||
regexp: '^#?\s*{{ item.key }}\b'
|
||||
line: "{{ item.key }}\t{{ item.value }}"
|
||||
loop:
|
||||
- {key: PASS_MAX_DAYS, value: "{{ cis_cfg.pass_max_days }}"}
|
||||
- {key: PASS_MIN_DAYS, value: "{{ cis_cfg.pass_min_days }}"}
|
||||
- {key: PASS_WARN_AGE, value: "{{ cis_cfg.pass_warn_age }}"}
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
|
||||
# account_disable_post_pw_expiration: lock accounts INACTIVE days after expiry.
|
||||
- name: Set the default account inactivity lock period
|
||||
when: cis_effective_rules.password_expiry | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/default/useradd
|
||||
regexp: '^\s*#?\s*INACTIVE\s*='
|
||||
line: "INACTIVE={{ cis_cfg.pass_inactive }}"
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
- name: Check CIS permission targets
|
||||
when: cis_effective_rules.file_permissions | default(false)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item.path }}"
|
||||
loop: "{{ cis_permission_targets }}"
|
||||
@@ -10,14 +9,12 @@
|
||||
changed_when: false
|
||||
|
||||
- name: Set permissions for existing targets
|
||||
when:
|
||||
- cis_effective_rules.file_permissions | default(false)
|
||||
- item.stat.exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.item.path }}"
|
||||
owner: "{{ item.item.owner | default(omit) }}"
|
||||
group: "{{ item.item.group | default(omit) }}"
|
||||
mode: "{{ item.item.mode }}"
|
||||
loop: "{{ cis_permission_stats.results | default([]) }}"
|
||||
loop: "{{ cis_permission_stats.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.path }}"
|
||||
when: item.stat.exists
|
||||
|
||||
@@ -1,218 +1,62 @@
|
||||
---
|
||||
- name: Restrict core dumps
|
||||
when: cis_effective_rules.core_dumps | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/limits.conf
|
||||
regexp: '^\*\s+hard\s+core\s+'
|
||||
line: "* hard core 0"
|
||||
|
||||
- name: Ensure the systemd coredump drop-in directory exists (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.core_dumps | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/systemd/coredump.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Disable systemd core dump storage and backtraces (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.core_dumps | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/coredump.conf.d/10-cis.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Coredump]
|
||||
Storage=none
|
||||
ProcessSizeMax=0
|
||||
|
||||
- name: Set password quality requirements
|
||||
when: cis_effective_rules.pwquality | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/pwquality.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- {regexp: '^\s*#?\s*minlen\s*=', line: "minlen = {{ cis_cfg.pwquality_minlen }}"}
|
||||
- {regexp: '^\s*#?\s*dcredit\s*=', line: "dcredit = -1"}
|
||||
- {regexp: '^\s*#?\s*ucredit\s*=', line: "ucredit = -1"}
|
||||
- {regexp: '^\s*#?\s*ocredit\s*=', line: "ocredit = -1"}
|
||||
- {regexp: '^\s*#?\s*lcredit\s*=', line: "lcredit = -1"}
|
||||
loop_control:
|
||||
label: "{{ item.line }}"
|
||||
|
||||
# Stricter complexity SSG cis_server_l1 checks; affects only new-password changes.
|
||||
- name: Set strict password quality requirements (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.pwquality | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/pwquality.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- {regexp: '^\s*#?\s*difok\s*=', line: "difok = {{ cis_cfg.pwquality_difok }}"}
|
||||
- {regexp: '^\s*#?\s*maxrepeat\s*=', line: "maxrepeat = {{ cis_cfg.pwquality_maxrepeat }}"}
|
||||
- {regexp: '^\s*#?\s*maxsequence\s*=', line: "maxsequence = {{ cis_cfg.pwquality_maxsequence }}"}
|
||||
- {regexp: '^\s*#?\s*minclass\s*=', line: "minclass = {{ cis_cfg.pwquality_minclass }}"}
|
||||
- {regexp: '^\s*#?\s*dictcheck\s*=', line: "dictcheck = {{ cis_cfg.pwquality_dictcheck }}"}
|
||||
- {regexp: '^\s*#?\s*enforce_for_root\b', line: "enforce_for_root"}
|
||||
loop_control:
|
||||
label: "{{ item.line }}"
|
||||
|
||||
- name: Set the default shell umask
|
||||
when: cis_effective_rules.umask_default | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*umask\s+\d+'
|
||||
line: "umask {{ cis_cfg.umask }}"
|
||||
|
||||
- name: Set the shell idle timeout
|
||||
when: cis_effective_rules.shell_timeout | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*(export\s+)?TMOUT='
|
||||
line: "export TMOUT={{ cis_cfg.tmout }}"
|
||||
|
||||
# A drop-in survives systemd upgrades; the RHEL vendor journald.conf does not.
|
||||
- name: Ensure the journald drop-in directory exists
|
||||
when: cis_effective_rules.journald_persistent | default(false)
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/systemd/journald.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Enable persistent journald storage
|
||||
when: cis_effective_rules.journald_persistent | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/journald.conf.d/10-cis.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
|
||||
- name: Compress large journald log files (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.journald_persistent | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/journald.conf.d/20-cis-compress.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Journal]
|
||||
Compress=yes
|
||||
|
||||
- name: Log sudo commands
|
||||
when: cis_effective_rules.sudo_logfile | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/sudoers
|
||||
regexp: '^\s*Defaults\s+logfile='
|
||||
line: 'Defaults logfile="/var/log/sudo.log"'
|
||||
|
||||
- name: Require a pty for sudo (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.sudo_logfile | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/sudoers
|
||||
regexp: '^\s*Defaults\s+use_pty\b'
|
||||
line: "Defaults use_pty"
|
||||
|
||||
- name: Restrict su to the wheel group
|
||||
when: cis_effective_rules.su_restriction | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/pam.d/su
|
||||
regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so'
|
||||
line: auth required pam_wheel.so
|
||||
|
||||
# authselect wires the pam_faillock stack via the feature; deny/unlock_time live
|
||||
# in faillock.conf, the supported place (pam_faillock(8) deprecates module args).
|
||||
- name: Configure account lockout (authselect)
|
||||
when:
|
||||
- cis_effective_rules.faillock | default(false)
|
||||
- is_authselect | bool
|
||||
block:
|
||||
- name: Enable the authselect faillock feature
|
||||
ansible.builtin.command: "{{ chroot_command }} authselect enable-feature with-faillock"
|
||||
register: cis_faillock_result
|
||||
changed_when: cis_faillock_result.rc == 0
|
||||
|
||||
- name: Set faillock thresholds
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/faillock.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
create: true
|
||||
mode: "0644"
|
||||
loop:
|
||||
- {regexp: '^\s*#?\s*deny\s*=', line: "deny = {{ cis_cfg.faillock_deny }}"}
|
||||
- {regexp: '^\s*#?\s*unlock_time\s*=', line: "unlock_time = {{ cis_cfg.faillock_unlock_time }}"}
|
||||
loop_control:
|
||||
label: "{{ item.line }}"
|
||||
|
||||
- name: Configure account lockout
|
||||
when:
|
||||
- cis_effective_rules.faillock | default(false)
|
||||
- not is_authselect | bool
|
||||
- name: Add Security related lines into config files
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
line: "{{ item.content }}"
|
||||
loop:
|
||||
- path: '/mnt/etc/{{ "pam.d/common-auth" if is_debian | bool else "pam.d/system-auth" }}'
|
||||
- { path: /mnt/etc/security/limits.conf, regexp: '^\*\s+hard\s+core\s+', content: "* hard core 0" }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*minlen\s*=', content: "minlen = {{ cis_cfg.pwquality_minlen }}" }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*dcredit\s*=', content: dcredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ucredit\s*=', content: ucredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ocredit\s*=', content: ocredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*lcredit\s*=', content: lcredit = -1 }
|
||||
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*umask\s+\d+'
|
||||
content: "umask {{ cis_cfg.umask }}"
|
||||
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*(export\s+)?TMOUT='
|
||||
content: "export TMOUT={{ cis_cfg.tmout }}"
|
||||
- path: '/mnt/{{ "usr/lib/systemd/journald.conf" if is_rhel | bool else "etc/systemd/journald.conf" }}'
|
||||
regexp: '^\s*#?\s*Storage='
|
||||
content: Storage=persistent
|
||||
- path: /mnt/etc/sudoers
|
||||
regexp: '^\s*Defaults\s+logfile='
|
||||
content: Defaults logfile="/var/log/sudo.log"
|
||||
- path: /mnt/etc/pam.d/su
|
||||
regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so'
|
||||
content: auth required pam_wheel.so
|
||||
- path: >-
|
||||
/mnt/etc/{{
|
||||
"pam.d/common-auth"
|
||||
if is_debian | bool
|
||||
else "authselect/system-auth"
|
||||
if os == "fedora"
|
||||
else "pam.d/system-auth"
|
||||
}}
|
||||
regexp: '^\s*auth\s+required\s+pam_faillock\.so'
|
||||
line: >-
|
||||
content: >-
|
||||
auth required pam_faillock.so onerr=fail audit silent deny={{ cis_cfg.faillock_deny }} unlock_time={{ cis_cfg.faillock_unlock_time }}
|
||||
- path: '/mnt/etc/{{ "pam.d/common-account" if is_debian | bool else "pam.d/system-auth" }}'
|
||||
- path: >-
|
||||
/mnt/etc/{{
|
||||
"pam.d/common-account"
|
||||
if is_debian | bool
|
||||
else "authselect/system-auth"
|
||||
if os == "fedora"
|
||||
else "pam.d/system-auth"
|
||||
}}
|
||||
regexp: '^\s*account\s+required\s+pam_faillock\.so'
|
||||
line: account required pam_faillock.so
|
||||
content: account required pam_faillock.so
|
||||
- path: >-
|
||||
/mnt/etc/pam.d/{{
|
||||
"common-password"
|
||||
if is_debian | bool
|
||||
else "passwd"
|
||||
}}
|
||||
regexp: '^\s*password\s+\[success=1.*\]\s+pam_unix\.so'
|
||||
content: >-
|
||||
password [success=1 default=ignore] pam_unix.so obscure sha512 remember={{ cis_cfg.password_remember }}
|
||||
- { path: /mnt/etc/hosts.deny, regexp: '^ALL:\s*ALL', content: "ALL: ALL" }
|
||||
- { path: /mnt/etc/hosts.allow, regexp: '^sshd:\s*ALL', content: "sshd: ALL" }
|
||||
loop_control:
|
||||
label: "{{ item.regexp }}"
|
||||
|
||||
- name: Enforce password history
|
||||
when: cis_effective_rules.password_history | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: >-
|
||||
/mnt/etc/pam.d/{{
|
||||
"common-password"
|
||||
if is_debian | bool
|
||||
else "passwd"
|
||||
}}
|
||||
regexp: '^\s*password\s+\[success=1.*\]\s+pam_unix\.so'
|
||||
line: >-
|
||||
password [success=1 default=ignore] pam_unix.so obscure sha512 remember={{ cis_cfg.password_remember }}
|
||||
|
||||
# SSG cis_server_l1 checks pam_pwhistory (not pam_unix remember) in the auth-stack
|
||||
# files; affects only password changes, so no login-lockout risk. EL9 has no
|
||||
# authselect path here (same direct-edit the faillock rule above uses).
|
||||
- name: Enforce password reuse limit via pam_pwhistory (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.password_history | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item }}"
|
||||
regexp: '^\s*password\s+(requisite|required)\s+pam_pwhistory\.so'
|
||||
line: "password requisite pam_pwhistory.so use_authtok remember={{ cis_cfg.pwhistory_remember }} enforce_for_root"
|
||||
insertbefore: '^\s*password\s+.*pam_unix\.so'
|
||||
loop: >-
|
||||
{{
|
||||
['/mnt/etc/pam.d/system-auth', '/mnt/etc/pam.d/password-auth']
|
||||
if is_rhel | bool
|
||||
else (['/mnt/etc/pam.d/common-password'] if is_debian | bool else [])
|
||||
}}
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
|
||||
- name: Configure TCP wrappers
|
||||
when: cis_effective_rules.tcp_wrappers | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- {path: /mnt/etc/hosts.deny, regexp: '^ALL:\s*ALL', line: "ALL: ALL"}
|
||||
- {path: /mnt/etc/hosts.allow, regexp: '^sshd:\s*ALL', line: "sshd: ALL"}
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
label: "{{ item.content }}"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
- name: Adjust SSHD config
|
||||
when: cis_effective_rules.sshd_hardening | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/ssh/sshd_config
|
||||
regexp: ^\s*#?{{ item.option }}\s+.*$
|
||||
@@ -10,7 +9,6 @@
|
||||
label: "{{ item.option }}"
|
||||
|
||||
- name: Detect target OpenSSH version
|
||||
when: cis_effective_rules.sshd_hardening | default(false)
|
||||
ansible.builtin.shell: >-
|
||||
set -o pipefail && {{ chroot_command }} ssh -V 2>&1 | grep -oP 'OpenSSH_\K[0-9]+\.[0-9]+'
|
||||
args:
|
||||
@@ -20,7 +18,6 @@
|
||||
failed_when: false
|
||||
|
||||
- name: Append CIS specific configurations to sshd_config
|
||||
when: cis_effective_rules.sshd_hardening | default(false)
|
||||
vars:
|
||||
cis_sshd_has_mlkem: "{{ (cis_sshd_openssh_version.stdout | default('0.0') is version('9.9', '>=')) }}"
|
||||
cis_sshd_kex: >-
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
---
|
||||
- name: Create a consolidated sysctl configuration file
|
||||
when: cis_effective_rules.sysctl_hardening | default(false)
|
||||
vars:
|
||||
# ipv6_disable is a separate rule: when off, drop the disable_ipv6 keys but keep the rest.
|
||||
_cis_sysctl: >-
|
||||
{{ cis_cfg.sysctl
|
||||
if (cis_effective_rules.ipv6_disable | default(false))
|
||||
else (cis_cfg.sysctl | dict2items | rejectattr('key', 'search', 'disable_ipv6') | items2dict) }}
|
||||
ansible.builtin.copy:
|
||||
# 99- so CIS wins: a 10- name loses to vendor /usr/lib/sysctl.d/10-default-yama-scope.conf
|
||||
# (later basename applies last), which reset kernel.yama.ptrace_scope back to 0.
|
||||
dest: /mnt/etc/sysctl.d/99-cis.conf
|
||||
dest: /mnt/etc/sysctl.d/10-cis.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
## CIS Sysctl configurations
|
||||
{% for key, value in _cis_sysctl | dictsort %}
|
||||
{% for key, value in cis_cfg.sysctl | dictsort %}
|
||||
{{ key }}={{ value }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
- name: Set login warning banners
|
||||
when: cis_effective_rules.warning_banners | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: "/mnt/etc/{{ item }}"
|
||||
content: "{{ cis_cfg.banner_text }}\n"
|
||||
mode: "0644"
|
||||
loop:
|
||||
- issue
|
||||
- issue.net
|
||||
- motd
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
# fusermount3 is the modern name; older distros still ship fusermount.
|
||||
# OS-specific binary names for CIS permission targets.
|
||||
# fusermount3 is the modern name; older distros still use fusermount.
|
||||
cis_fusermount_binary: >-
|
||||
{{
|
||||
'fusermount3'
|
||||
@@ -18,235 +19,3 @@ cis_write_binary: >-
|
||||
if (os == 'debian' and (os_version | string) == '11')
|
||||
else 'write'
|
||||
}}
|
||||
|
||||
cis_pkg_install: >-
|
||||
{{ chroot_command }} {{
|
||||
'apt-get install -y'
|
||||
if is_debian | bool
|
||||
else 'pacman -S --noconfirm'
|
||||
if os == 'archlinux'
|
||||
else 'dnf install -y'
|
||||
}}
|
||||
|
||||
# Rule catalog: control -> CIS level + whether a task implements it.
|
||||
# `default` enables only implemented rules; `l1`/`l2` add the level-tagged ones.
|
||||
cis_rule_catalog:
|
||||
module_blacklist: {level: l1, implemented: true} # fs/net modprobe blacklist (list per profile)
|
||||
usb_lockdown: {level: l2, implemented: true} # udev authorized_default=0 (aggressive)
|
||||
sysctl_hardening: {level: l1, implemented: true}
|
||||
ipv6_disable: {level: l2, implemented: true} # disable_ipv6 subset of the sysctl set
|
||||
umask_default: {level: l1, implemented: true}
|
||||
empty_password_login: {level: l1, implemented: true}
|
||||
pwquality: {level: l1, implemented: true}
|
||||
core_dumps: {level: l1, implemented: true}
|
||||
shell_timeout: {level: l1, implemented: true}
|
||||
journald_persistent: {level: l1, implemented: true}
|
||||
sudo_logfile: {level: l1, implemented: true}
|
||||
su_restriction: {level: l1, implemented: true}
|
||||
faillock: {level: l1, implemented: true}
|
||||
password_history: {level: l1, implemented: true}
|
||||
tcp_wrappers: {level: l1, implemented: true}
|
||||
crypto_policy: {level: l1, implemented: true} # RedHat non-Fedora only
|
||||
mask_services: {level: l1, implemented: true}
|
||||
cron_at_access: {level: l1, implemented: true}
|
||||
file_permissions: {level: l1, implemented: true}
|
||||
sshd_hardening: {level: l1, implemented: true}
|
||||
password_expiry: {level: l1, implemented: true} # login.defs aging policy
|
||||
aide: {level: l1, implemented: true} # file-integrity db + daily check
|
||||
warning_banners: {level: l1, implemented: true} # /etc/issue, issue.net, motd
|
||||
auditd: {level: l2, implemented: true} # audit daemon + CIS rule set
|
||||
grub_password: {level: l1, implemented: true} # opt-in only; needs params.grub_password_hash
|
||||
|
||||
# Rules not listed are off. A per-host system.features.cis.rules map overlays this.
|
||||
cis_profiles:
|
||||
# default = established house behaviour, kept byte-for-byte unchanged.
|
||||
default:
|
||||
module_blacklist: true
|
||||
usb_lockdown: true
|
||||
sysctl_hardening: true
|
||||
ipv6_disable: true
|
||||
umask_default: true
|
||||
empty_password_login: true
|
||||
pwquality: true
|
||||
core_dumps: true
|
||||
shell_timeout: true
|
||||
journald_persistent: true
|
||||
sudo_logfile: true
|
||||
su_restriction: true
|
||||
faillock: true
|
||||
password_history: true
|
||||
tcp_wrappers: true
|
||||
crypto_policy: true
|
||||
mask_services: true
|
||||
cron_at_access: true
|
||||
file_permissions: true
|
||||
sshd_hardening: true
|
||||
# l1 = clean CIS Level 1: drops the L2 extras (usb_lockdown, ipv6_disable).
|
||||
l1:
|
||||
module_blacklist: true
|
||||
sysctl_hardening: true
|
||||
umask_default: true
|
||||
empty_password_login: true
|
||||
pwquality: true
|
||||
core_dumps: true
|
||||
shell_timeout: true
|
||||
journald_persistent: true
|
||||
sudo_logfile: true
|
||||
su_restriction: true
|
||||
faillock: true
|
||||
password_history: true
|
||||
tcp_wrappers: true
|
||||
crypto_policy: true
|
||||
mask_services: true
|
||||
cron_at_access: true
|
||||
file_permissions: true
|
||||
sshd_hardening: true
|
||||
password_expiry: true
|
||||
aide: true
|
||||
warning_banners: true
|
||||
# l2 = l1 plus the defence-in-depth Level 2 controls.
|
||||
l2:
|
||||
module_blacklist: true
|
||||
usb_lockdown: true
|
||||
sysctl_hardening: true
|
||||
ipv6_disable: true
|
||||
umask_default: true
|
||||
empty_password_login: true
|
||||
pwquality: true
|
||||
core_dumps: true
|
||||
shell_timeout: true
|
||||
journald_persistent: true
|
||||
sudo_logfile: true
|
||||
su_restriction: true
|
||||
faillock: true
|
||||
password_history: true
|
||||
tcp_wrappers: true
|
||||
crypto_policy: true
|
||||
mask_services: true
|
||||
cron_at_access: true
|
||||
file_permissions: true
|
||||
sshd_hardening: true
|
||||
password_expiry: true
|
||||
aide: true
|
||||
warning_banners: true
|
||||
auditd: true
|
||||
|
||||
# Override per host via system.features.cis.params: dicts deep-merge,
|
||||
# list-valued keys (e.g. sshd_options) replace wholesale.
|
||||
cis_param_defaults:
|
||||
modules_blacklist:
|
||||
- freevxfs
|
||||
- jffs2
|
||||
- hfs
|
||||
- hfsplus
|
||||
- cramfs
|
||||
- udf
|
||||
- usb-storage
|
||||
- dccp
|
||||
- sctp
|
||||
- rds
|
||||
- tipc
|
||||
- firewire-core
|
||||
- firewire-sbp2
|
||||
- thunderbolt
|
||||
sysctl:
|
||||
fs.suid_dumpable: 0
|
||||
kernel.dmesg_restrict: 1
|
||||
kernel.kptr_restrict: 2
|
||||
kernel.perf_event_paranoid: 3
|
||||
kernel.unprivileged_bpf_disabled: 1
|
||||
kernel.yama.ptrace_scope: 2
|
||||
kernel.randomize_va_space: 2
|
||||
net.ipv4.ip_forward: 0
|
||||
net.ipv4.tcp_syncookies: 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts: 1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses: 1
|
||||
net.ipv4.conf.all.log_martians: 1
|
||||
net.ipv4.conf.all.rp_filter: 1
|
||||
net.ipv4.conf.all.secure_redirects: 0
|
||||
net.ipv4.conf.all.send_redirects: 0
|
||||
net.ipv4.conf.all.accept_redirects: 0
|
||||
net.ipv4.conf.all.accept_source_route: 0
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
net.ipv4.conf.default.log_martians: 1
|
||||
net.ipv4.conf.default.rp_filter: 1
|
||||
net.ipv4.conf.default.secure_redirects: 0
|
||||
net.ipv4.conf.default.send_redirects: 0
|
||||
net.ipv4.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.all.accept_redirects: 0
|
||||
net.ipv6.conf.all.disable_ipv6: 1
|
||||
net.ipv6.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.default.disable_ipv6: 1
|
||||
net.ipv6.conf.lo.disable_ipv6: 1
|
||||
sshd_options:
|
||||
- {option: LogLevel, value: VERBOSE}
|
||||
- {option: LoginGraceTime, value: "60"}
|
||||
- {option: PermitRootLogin, value: "no"}
|
||||
- {option: StrictModes, value: "yes"}
|
||||
- {option: MaxAuthTries, value: "4"}
|
||||
- {option: MaxSessions, value: "10"}
|
||||
- {option: MaxStartups, value: "10:30:60"}
|
||||
- {option: PubkeyAuthentication, value: "yes"}
|
||||
- {option: HostbasedAuthentication, value: "no"}
|
||||
- {option: IgnoreRhosts, value: "yes"}
|
||||
- {option: PasswordAuthentication, value: "no"}
|
||||
- {option: PermitEmptyPasswords, value: "no"}
|
||||
- {option: KerberosAuthentication, value: "no"}
|
||||
- {option: GSSAPIAuthentication, value: "no"}
|
||||
- {option: AllowAgentForwarding, value: "no"}
|
||||
- {option: AllowTcpForwarding, value: "no"}
|
||||
- {option: KbdInteractiveAuthentication, value: "no"}
|
||||
- {option: GatewayPorts, value: "no"}
|
||||
- {option: X11Forwarding, value: "no"}
|
||||
- {option: PermitUserEnvironment, value: "no"}
|
||||
- {option: ClientAliveInterval, value: "300"}
|
||||
- {option: ClientAliveCountMax, value: "1"}
|
||||
- {option: PermitTunnel, value: "no"}
|
||||
- {option: Banner, value: /etc/issue.net}
|
||||
pwquality_minlen: 14
|
||||
# pwquality strict set (l1/l2 only, cis_strict): SSG cis_server_l1 values.
|
||||
pwquality_difok: 2
|
||||
pwquality_maxrepeat: 3
|
||||
pwquality_maxsequence: 3
|
||||
pwquality_minclass: 4
|
||||
pwquality_dictcheck: 1
|
||||
tmout: 900
|
||||
umask: "077"
|
||||
umask_profile: "027"
|
||||
faillock_deny: 5
|
||||
faillock_unlock_time: 900
|
||||
password_remember: 5
|
||||
# pwhistory remember (l1/l2 only, cis_strict): SSG wants 24 via pam_pwhistory.
|
||||
pwhistory_remember: 24
|
||||
# password_expiry (l1/l2): /etc/login.defs aging.
|
||||
pass_max_days: 365
|
||||
pass_min_days: 1
|
||||
pass_warn_age: 7
|
||||
# account_disable_post_pw_expiration (l1/l2): days after expiry to lock (SSG=45).
|
||||
pass_inactive: 45
|
||||
# aide (l1/l2): daily integrity-check schedule.
|
||||
aide_cron_hour: "5"
|
||||
aide_cron_minute: "0"
|
||||
# warning_banners (l1/l2): login/MOTD text.
|
||||
banner_text: "Authorized access only. All activity may be monitored and reported."
|
||||
# grub_password (opt-in only): a grub2 pbkdf2 hash; empty unless opted in.
|
||||
grub_password_hash: ""
|
||||
# insecure_packages (l1/l2 only, cis_strict): legacy cleartext clients to remove.
|
||||
insecure_packages:
|
||||
- telnet
|
||||
|
||||
# Only the module blacklist differs by profile: l1 trims to the L1 filesystem
|
||||
# modules; default/l2 keep the full list.
|
||||
cis_profile_params:
|
||||
default: {}
|
||||
l1:
|
||||
modules_blacklist:
|
||||
- cramfs
|
||||
- freevxfs
|
||||
- jffs2
|
||||
- hfs
|
||||
- hfsplus
|
||||
- udf
|
||||
- usb-storage
|
||||
l2: {}
|
||||
|
||||
@@ -72,12 +72,6 @@
|
||||
| trim
|
||||
}}
|
||||
|
||||
- name: Ensure boot device is set to hard disk in VM XML
|
||||
when: "'<boot ' not in cleanup_libvirt_domain_xml_clean"
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml_clean: >-
|
||||
{{ cleanup_libvirt_domain_xml_clean | regex_replace('(</type>)', '\1\n <boot dev="hd"/>') }}
|
||||
|
||||
- name: Update VM definition without installer media
|
||||
community.libvirt.virt:
|
||||
command: define
|
||||
@@ -94,35 +88,6 @@
|
||||
state: destroyed
|
||||
failed_when: false
|
||||
|
||||
- name: Enroll Secure Boot keys in VM NVRAM
|
||||
when:
|
||||
- system_cfg.features.secure_boot.enabled | default(false) | bool
|
||||
- os != 'archlinux'
|
||||
block:
|
||||
- name: Find VM NVRAM file path
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail &&
|
||||
virsh -c {{ libvirt_uri | default('qemu:///system') }} dumpxml {{ hostname }}
|
||||
| grep -oP '<nvram[^>]*>\K[^<]+'
|
||||
executable: /bin/bash
|
||||
register: _sb_nvram_path
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Enroll Secure Boot keys via virt-fw-vars
|
||||
when: _sb_nvram_path.stdout | default('') | length > 0
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- virt-fw-vars
|
||||
- --inplace
|
||||
- "{{ _sb_nvram_path.stdout | trim }}"
|
||||
- --enroll-redhat
|
||||
- --secure-boot
|
||||
register: _sb_enroll_result
|
||||
changed_when: _sb_enroll_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Start the VM
|
||||
community.libvirt.virt:
|
||||
name: "{{ hostname }}"
|
||||
|
||||
@@ -16,20 +16,13 @@
|
||||
loop: >-
|
||||
{{
|
||||
['ide0', 'ide2']
|
||||
+ (['ide1'] if not (os == 'rhel' and system_cfg.content.source == 'dvd') else [])
|
||||
+ (['ide1'] if not (os == 'rhel' and system_cfg.features.rhel_repo.source == 'iso') else [])
|
||||
}}
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Ensure the installer environment is powered off
|
||||
- name: Start the VM
|
||||
community.proxmox.proxmox_kvm:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
state: stopped
|
||||
force: true
|
||||
no_log: true
|
||||
|
||||
- name: Boot the installed OS
|
||||
community.proxmox.proxmox_kvm:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
state: started
|
||||
state: restarted
|
||||
no_log: true
|
||||
|
||||
@@ -2,16 +2,6 @@
|
||||
- name: Unmount Disks
|
||||
become: true
|
||||
block:
|
||||
- name: Unmount the bootstrap package cache
|
||||
ansible.posix.mount:
|
||||
path: /mnt/var/cache
|
||||
state: unmounted
|
||||
|
||||
- name: Remove the bootstrap package cache so it is not sealed into the image
|
||||
ansible.builtin.file:
|
||||
path: /mnt/.bootstrap-cache
|
||||
state: absent
|
||||
|
||||
- name: Disable Swap
|
||||
ansible.builtin.command: swapoff -a
|
||||
register: cleanup_swapoff_result
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
]
|
||||
if (rhel_iso is defined and rhel_iso | length > 0
|
||||
and not (os == 'rhel' and system_cfg.content.source == 'dvd'))
|
||||
and not (os == 'rhel' and system_cfg.features.rhel_repo.source == 'iso'))
|
||||
else []
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
# Network backend is detected per host from the target rootfs in network.yml;
|
||||
# no static map needed.
|
||||
# Network configuration dispatch — maps OS name to the task file
|
||||
# that writes network config. Default (NetworkManager) applies to
|
||||
# all OSes not explicitly listed.
|
||||
configuration_network_task_map:
|
||||
alpine: network_alpine.yml
|
||||
void: network_void.yml
|
||||
|
||||
@@ -14,12 +14,3 @@
|
||||
- name: Set platform configuration
|
||||
ansible.builtin.set_fact:
|
||||
_configuration_platform: "{{ configuration_platform_config[os_family] }}"
|
||||
|
||||
- name: Override EFI loader to shim for Secure Boot
|
||||
when:
|
||||
- system_cfg.features.secure_boot.enabled | bool
|
||||
- _configuration_platform.efi_loader != 'shimx64.efi'
|
||||
- os != 'archlinux'
|
||||
ansible.builtin.set_fact:
|
||||
_configuration_platform: >-
|
||||
{{ _configuration_platform | combine({'efi_loader': 'shimx64.efi'}) }}
|
||||
|
||||
@@ -42,37 +42,25 @@
|
||||
- name: Configure sudo banner
|
||||
when: system_cfg.features.banner.sudo | bool
|
||||
block:
|
||||
- name: Detect the target sudo implementation
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/bin/sudo --version"
|
||||
register: configuration_sudo_version
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
- name: Create sudo lecture file
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
I am Groot, and I know what I'm doing.
|
||||
dest: /mnt/etc/sudo_lecture
|
||||
mode: "0644"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
# sudo-rs (Ubuntu 25.10+) implements neither `lecture` nor `lecture_file`
|
||||
# and warns on every sudo call when they are set. It prints its version banner
|
||||
# to stderr, not stdout, so match against both streams.
|
||||
- name: Configure the sudo lecture
|
||||
when: "'sudo-rs' not in (configuration_sudo_version.stdout ~ configuration_sudo_version.stderr)"
|
||||
block:
|
||||
- name: Create sudo lecture file
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
I am Groot, and I know what I'm doing.
|
||||
dest: /mnt/etc/sudo_lecture
|
||||
mode: "0644"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Enable sudo lecture in sudoers
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/sudoers
|
||||
line: "{{ item }}"
|
||||
state: present
|
||||
create: true
|
||||
mode: "0440"
|
||||
owner: root
|
||||
group: root
|
||||
validate: "/usr/sbin/visudo --check --file=%s"
|
||||
loop:
|
||||
- "Defaults lecture=always"
|
||||
- "Defaults lecture_file=/etc/sudo_lecture"
|
||||
- name: Enable sudo lecture in sudoers
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/sudoers
|
||||
line: "{{ item }}"
|
||||
state: present
|
||||
create: true
|
||||
mode: "0440"
|
||||
owner: root
|
||||
group: root
|
||||
validate: "/usr/sbin/visudo --check --file=%s"
|
||||
loop:
|
||||
- "Defaults lecture=always"
|
||||
- "Defaults lecture_file=/etc/sudo_lecture"
|
||||
|
||||
@@ -34,16 +34,6 @@
|
||||
register: configuration_efi_entry_result
|
||||
changed_when: configuration_efi_entry_result.rc == 0
|
||||
|
||||
- name: Set installed OS as first EFI boot entry
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail &&
|
||||
efibootmgr | grep -i '{{ _efi_vendor }}' | grep -oP 'Boot\K[0-9A-F]+' | head -1
|
||||
| xargs -I{} efibootmgr -o {}
|
||||
executable: /bin/bash
|
||||
register: _efi_bootorder_result
|
||||
changed_when: _efi_bootorder_result.rc == 0
|
||||
|
||||
- name: Ensure lvm2 for non btrfs filesystems
|
||||
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
|
||||
ansible.builtin.lineinfile:
|
||||
@@ -58,62 +48,16 @@
|
||||
register: configuration_initramfs_result
|
||||
changed_when: configuration_initramfs_result.rc == 0
|
||||
|
||||
- name: Generate grub config (RedHat)
|
||||
when: os_family == 'RedHat'
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/sbin/{{ _configuration_platform.grub_mkconfig_prefix }}
|
||||
-o /boot/grub2/grub.cfg
|
||||
register: configuration_grub_result
|
||||
changed_when: configuration_grub_result.rc == 0
|
||||
|
||||
- name: Fix btrfs BLS boot variable in grub config
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.filesystem == 'btrfs'
|
||||
ansible.builtin.replace:
|
||||
path: /mnt/boot/grub2/grub.cfg
|
||||
regexp: 'search --no-floppy --fs-uuid --set=boot \S+'
|
||||
replace: 'set boot=$root'
|
||||
|
||||
- name: Create EFI grub.cfg wrapper for RedHat
|
||||
when: os_family == 'RedHat'
|
||||
- name: Generate grub config
|
||||
vars:
|
||||
_grub2_path: >-
|
||||
configuration_grub_cfg_cmd: >-
|
||||
{{
|
||||
'/grub2'
|
||||
if (partitioning_separate_boot | bool)
|
||||
else ('/@/boot/grub2' if system_cfg.filesystem == 'btrfs' else '/boot/grub2')
|
||||
'/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o '
|
||||
+ partitioning_efi_mountpoint
|
||||
+ '/EFI/' + _efi_vendor + '/grub.cfg'
|
||||
if os_family == 'RedHat'
|
||||
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
|
||||
}}
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
uuid=$(grep -m1 'search.*--set=root' /mnt/boot/grub2/grub.cfg | grep -oP '[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}')
|
||||
cat > /mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grub.cfg <<GRUBEOF
|
||||
search --no-floppy --fs-uuid --set=dev $uuid
|
||||
set prefix=(\$dev){{ _grub2_path }}
|
||||
export \$prefix
|
||||
configfile \$prefix/grub.cfg
|
||||
GRUBEOF
|
||||
executable: /bin/bash
|
||||
register: _grub_wrapper_result
|
||||
changed_when: _grub_wrapper_result.rc == 0
|
||||
|
||||
- name: Generate grub config (non-RedHat)
|
||||
when: os_family != 'RedHat'
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg"
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}"
|
||||
register: configuration_grub_result
|
||||
changed_when: configuration_grub_result.rc == 0
|
||||
|
||||
- name: Rebuild GRUB as standalone EFI for Secure Boot
|
||||
when:
|
||||
- system_cfg.features.secure_boot.enabled | default(false) | bool
|
||||
- os == 'archlinux'
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} grub-mkstandalone
|
||||
-d /usr/lib/grub/x86_64-efi
|
||||
-O x86_64-efi
|
||||
--disable-shim-lock
|
||||
-o {{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grubx64.efi
|
||||
boot/grub/grub.cfg=/boot/grub/grub.cfg
|
||||
register: _grub_standalone_result
|
||||
changed_when: _grub_standalone_result.rc == 0
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
block:
|
||||
- name: Set LUKS configuration facts
|
||||
vars:
|
||||
_raw_pcrs: >-
|
||||
luks_tpm2_pcrs: >-
|
||||
{{
|
||||
(
|
||||
system_cfg.luks.tpm2.pcrs
|
||||
@@ -20,17 +20,6 @@
|
||||
| regex_replace('\\s+', '')
|
||||
| regex_replace('^\\+|\\+$', '')
|
||||
}}
|
||||
_sb_pcr7_safe: >-
|
||||
{{
|
||||
system_cfg.features.secure_boot.enabled | bool
|
||||
and system_cfg.type | default('virtual') != 'virtual'
|
||||
}}
|
||||
luks_tpm2_pcrs: >-
|
||||
{{
|
||||
_raw_pcrs
|
||||
if _raw_pcrs | length > 0
|
||||
else ('7' if (_sb_pcr7_safe | bool) else '')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_luks_mapper_name: "{{ system_cfg.luks.mapper }}"
|
||||
configuration_luks_uuid: "{{ partitioning_luks_uuid | default('') }}"
|
||||
@@ -47,12 +36,6 @@
|
||||
configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}"
|
||||
configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}"
|
||||
configuration_luks_keyfile_path: "/etc/cryptsetup-keys.d/{{ system_cfg.luks.mapper }}.key"
|
||||
configuration_luks_tpm2_token_lib: >-
|
||||
{{
|
||||
'/usr/lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
|
||||
if os_family == 'Debian'
|
||||
else '/usr/lib64/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
|
||||
}}
|
||||
|
||||
- name: Validate LUKS UUID is available
|
||||
ansible.builtin.assert:
|
||||
@@ -68,13 +51,8 @@
|
||||
fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt.
|
||||
no_log: true
|
||||
|
||||
- name: Detect TPM2 unlock method
|
||||
ansible.builtin.include_tasks: encryption/initramfs_detect.yml
|
||||
|
||||
- name: Enroll TPM2 via systemd-cryptenroll
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('systemd-cryptenroll') == 'systemd-cryptenroll'
|
||||
- name: Enroll TPM2 for LUKS
|
||||
when: configuration_luks_auto_method == 'tpm2'
|
||||
ansible.builtin.include_tasks: encryption/tpm2.yml
|
||||
|
||||
- name: Configure LUKS keyfile auto-decrypt
|
||||
@@ -100,7 +78,7 @@
|
||||
}}
|
||||
luks_tpm2_option_list: >-
|
||||
{{
|
||||
(configuration_luks_auto_method == 'tpm2' and (_tpm2_method | default('systemd-cryptenroll')) == 'systemd-cryptenroll')
|
||||
(configuration_luks_auto_method == 'tpm2')
|
||||
| ternary(
|
||||
['tpm2-device=' + configuration_luks_tpm2_device]
|
||||
+ (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||
@@ -144,16 +122,16 @@
|
||||
path: /mnt{{ configuration_luks_keyfile_path }}
|
||||
state: absent
|
||||
|
||||
- name: Configure initramfs for LUKS
|
||||
ansible.builtin.include_tasks: encryption/initramfs.yml
|
||||
|
||||
- name: Configure crypttab
|
||||
ansible.builtin.include_tasks: encryption/crypttab.yml
|
||||
|
||||
- name: Configure dracut for LUKS
|
||||
when: _initramfs_generator | default('') == 'dracut'
|
||||
- name: Configure initramfs
|
||||
ansible.builtin.include_tasks: encryption/initramfs.yml
|
||||
|
||||
- name: Configure dracut
|
||||
when: os_family == 'RedHat'
|
||||
ansible.builtin.include_tasks: encryption/dracut.yml
|
||||
|
||||
- name: Configure GRUB for LUKS
|
||||
when: _initramfs_generator | default('') != 'dracut'
|
||||
when: not os_family == 'RedHat'
|
||||
ansible.builtin.include_tasks: encryption/grub.yml
|
||||
|
||||
@@ -9,57 +9,48 @@
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/dracut.conf.d/crypt.conf
|
||||
content: |
|
||||
add_dracutmodules+=" crypt systemd "
|
||||
{% if configuration_luks_keyfile_in_use | default(false) %}
|
||||
add_dracutmodules+=" crypt "
|
||||
{% if configuration_luks_keyfile_in_use %}
|
||||
install_items+=" {{ configuration_luks_keyfile_path }} "
|
||||
{% endif %}
|
||||
{% if configuration_luks_auto_method == 'tpm2' %}
|
||||
add_dracutmodules+=" tpm2-tss "
|
||||
install_items+=" {{ configuration_luks_tpm2_token_lib | default('') }} "
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Ensure kernel cmdline directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/kernel
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Read existing kernel cmdline
|
||||
- name: Read kernel cmdline defaults
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/kernel/cmdline
|
||||
register: _kernel_cmdline_slurp
|
||||
failed_when: false
|
||||
register: configuration_kernel_cmdline_slurp
|
||||
|
||||
- name: Build kernel cmdline with LUKS args
|
||||
vars:
|
||||
_cmdline_current: >-
|
||||
{{ (_kernel_cmdline_slurp.content | default('') | b64decode | default('')) | trim }}
|
||||
_cmdline_list: >-
|
||||
{{ _cmdline_current.split() if _cmdline_current | length > 0 else [] }}
|
||||
_cmdline_filtered: >-
|
||||
kernel_cmdline_current: >-
|
||||
{{ configuration_kernel_cmdline_slurp.content | b64decode | trim }}
|
||||
kernel_cmdline_list: >-
|
||||
{{
|
||||
_cmdline_list
|
||||
kernel_cmdline_current.split()
|
||||
if kernel_cmdline_current | length > 0 else []
|
||||
}}
|
||||
kernel_cmdline_filtered: >-
|
||||
{{
|
||||
kernel_cmdline_list
|
||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||
| list
|
||||
}}
|
||||
_cmdline_new: >-
|
||||
kernel_cmdline_new: >-
|
||||
{{
|
||||
(_cmdline_filtered + configuration_luks_kernel_args.split())
|
||||
(kernel_cmdline_filtered + configuration_luks_kernel_args.split())
|
||||
| unique
|
||||
| join(' ')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_dracut_kernel_cmdline: "{{ _cmdline_new }}"
|
||||
configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}"
|
||||
|
||||
- name: Write kernel cmdline with LUKS args
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/kernel/cmdline
|
||||
mode: "0644"
|
||||
content: "{{ _dracut_kernel_cmdline }}\n"
|
||||
content: "{{ configuration_kernel_cmdline_new }}\n"
|
||||
|
||||
- name: Update BLS entries with LUKS kernel cmdline
|
||||
when: os_family == 'RedHat'
|
||||
vars:
|
||||
_bls_cmdline: "{{ _dracut_kernel_cmdline }}"
|
||||
_bls_cmdline: "{{ configuration_kernel_cmdline_new }}"
|
||||
ansible.builtin.include_tasks: ../_bls_update.yml
|
||||
|
||||
@@ -1,104 +1,8 @@
|
||||
---
|
||||
# Initramfs configuration for LUKS auto-unlock.
|
||||
# Runs AFTER Build LUKS parameters (so configuration_luks_keyfile_in_use is set).
|
||||
# _initramfs_generator and _tpm2_method are set by initramfs_detect.yml.
|
||||
|
||||
# --- clevis: install and bind TPM2 ---
|
||||
- name: Install clevis in target system
|
||||
- name: Ensure keyfile pattern for initramfs-tools
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
vars:
|
||||
_clevis_install_cmd:
|
||||
Debian: >-
|
||||
{{ chroot_command }} apt install -y
|
||||
clevis clevis-luks clevis-tpm2 clevis-initramfs tpm2-tools
|
||||
RedHat: >-
|
||||
{{ chroot_command }} dnf install -y
|
||||
clevis clevis-luks clevis-systemd tpm2-tools
|
||||
Archlinux: >-
|
||||
{{ chroot_command }} pacman -S --noconfirm --needed
|
||||
clevis tpm2-tools
|
||||
ansible.builtin.command: "{{ _clevis_install_cmd[os_family] }}"
|
||||
register: _clevis_install_result
|
||||
changed_when: _clevis_install_result.rc == 0
|
||||
|
||||
- name: Install clevis on installer for LUKS binding
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
community.general.pacman:
|
||||
name:
|
||||
- clevis
|
||||
- tpm2-tools
|
||||
state: present
|
||||
retries: 3
|
||||
delay: 5
|
||||
|
||||
- name: Create clevis passphrase file
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/root/.luks-enroll-key
|
||||
content: "{{ configuration_luks_passphrase }}"
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Ensure TPM device accessible for clevis
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.shell: >-
|
||||
ls /mnt/dev/tpmrm0 2>/dev/null
|
||||
|| (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0)
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Bind LUKS to TPM2 via clevis
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
vars:
|
||||
_clevis_config: >-
|
||||
{{
|
||||
'{"pcr_ids":"' + configuration_luks_tpm2_pcrs + '"}'
|
||||
if configuration_luks_tpm2_pcrs | length > 0
|
||||
else '{}'
|
||||
}}
|
||||
ansible.builtin.command: >-
|
||||
clevis luks bind -f -k /mnt/root/.luks-enroll-key
|
||||
-d {{ configuration_luks_device }} tpm2 '{{ _clevis_config }}'
|
||||
register: _clevis_bind_result
|
||||
changed_when: _clevis_bind_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
# Initramfs regeneration is handled by the bootloader task which runs after
|
||||
# encryption configuration. Clevis hooks are included automatically by
|
||||
# update-initramfs when clevis-initramfs is installed.
|
||||
|
||||
- name: Remove clevis passphrase file
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.file:
|
||||
path: /mnt/root/.luks-enroll-key
|
||||
state: absent
|
||||
|
||||
- name: Report clevis binding result
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ 'Clevis TPM2 binding succeeded' if (_clevis_bind_result.rc | default(1)) == 0
|
||||
else 'Clevis TPM2 binding failed: ' + (_clevis_bind_result.stderr | default('unknown')) + '. System will require passphrase at boot.' }}
|
||||
|
||||
# --- initramfs-tools: keyfile support (non-TPM2) ---
|
||||
- name: Configure initramfs-tools keyfile pattern
|
||||
when:
|
||||
- _initramfs_generator | default('') == 'initramfs-tools'
|
||||
- configuration_luks_keyfile_in_use | default(false) | bool
|
||||
- os_family == 'Debian'
|
||||
- configuration_luks_keyfile_in_use
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/cryptsetup-initramfs/conf-hook
|
||||
regexp: "^KEYFILE_PATTERN="
|
||||
@@ -106,9 +10,8 @@
|
||||
create: true
|
||||
mode: "0644"
|
||||
|
||||
# --- mkinitcpio: systemd + sd-encrypt hooks ---
|
||||
- name: Configure mkinitcpio hooks for LUKS
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/mkinitcpio.conf
|
||||
regexp: "^HOOKS="
|
||||
@@ -117,13 +20,13 @@
|
||||
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
|
||||
|
||||
- name: Read mkinitcpio configuration
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/mkinitcpio.conf
|
||||
register: configuration_mkinitcpio_slurp
|
||||
|
||||
- name: Build mkinitcpio FILES list
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
when: os == 'archlinux'
|
||||
vars:
|
||||
mkinitcpio_files_list: >-
|
||||
{{
|
||||
@@ -139,7 +42,7 @@
|
||||
{{
|
||||
(
|
||||
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
|
||||
if (configuration_luks_keyfile_in_use | default(false))
|
||||
if configuration_luks_keyfile_in_use
|
||||
else (
|
||||
mkinitcpio_files_list
|
||||
| reject('equalto', configuration_luks_keyfile_path)
|
||||
@@ -152,7 +55,7 @@
|
||||
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
|
||||
|
||||
- name: Configure mkinitcpio FILES list
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/mkinitcpio.conf
|
||||
regexp: "^FILES="
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
# Resolve initramfs generator and TPM2 unlock method.
|
||||
# Sets _initramfs_generator and _tpm2_method facts.
|
||||
#
|
||||
# Generator detection: derived from the platform's initramfs_cmd
|
||||
# (dracut -> dracut, mkinitcpio -> mkinitcpio, else -> initramfs-tools)
|
||||
# TPM2 method: systemd-cryptenroll when generator supports tpm2-device,
|
||||
# clevis fallback otherwise. Non-native dracut installed automatically.
|
||||
|
||||
- name: Resolve initramfs generator
|
||||
vars:
|
||||
_user_generator: "{{ system_cfg.features.initramfs.generator | default('') }}"
|
||||
_native_generator: >-
|
||||
{{
|
||||
'dracut' if _configuration_platform.initramfs_cmd is search('dracut')
|
||||
else ('mkinitcpio' if _configuration_platform.initramfs_cmd is search('mkinitcpio')
|
||||
else 'initramfs-tools')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_initramfs_generator: >-
|
||||
{{ _user_generator if _user_generator | length > 0 else _native_generator }}
|
||||
_initramfs_native_generator: "{{ _native_generator }}"
|
||||
|
||||
# --- Install non-native dracut if overridden or needed ---
|
||||
- name: Install dracut in chroot when not native
|
||||
when:
|
||||
- _initramfs_generator == 'dracut'
|
||||
- _initramfs_native_generator != 'dracut'
|
||||
ansible.builtin.shell: >-
|
||||
{{ chroot_command }} sh -c '
|
||||
command -v apt >/dev/null 2>&1 && apt install -y dracut ||
|
||||
command -v pacman >/dev/null 2>&1 && pacman -S --noconfirm dracut ||
|
||||
command -v dnf >/dev/null 2>&1 && dnf install -y dracut
|
||||
'
|
||||
register: _dracut_install_result
|
||||
changed_when: _dracut_install_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Override initramfs command to dracut
|
||||
when:
|
||||
- _initramfs_generator == 'dracut'
|
||||
- _initramfs_native_generator != 'dracut'
|
||||
vars:
|
||||
# Generate dracut initramfs with output name matching what GRUB expects:
|
||||
# mkinitcpio native: /boot/initramfs-linux.img (Arch convention)
|
||||
# initramfs-tools native: /boot/initrd.img-<kver> (Debian convention)
|
||||
_dracut_cmd: >-
|
||||
{{
|
||||
'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initramfs-linux.img $kver; done"'
|
||||
if _initramfs_native_generator == 'mkinitcpio'
|
||||
else 'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"'
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_configuration_platform: >-
|
||||
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
|
||||
|
||||
# --- TPM2 method detection ---
|
||||
- name: Probe dracut for TPM2 module support
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _initramfs_generator != 'mkinitcpio'
|
||||
ansible.builtin.command: "{{ chroot_command }} dracut --list-modules"
|
||||
register: _dracut_modules_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Resolve TPM2 unlock method
|
||||
when: configuration_luks_auto_method == 'tpm2'
|
||||
vars:
|
||||
# mkinitcpio sd-encrypt supports tpm2-device natively
|
||||
# dracut with tpm2-tss module supports tpm2-device natively
|
||||
# everything else needs clevis
|
||||
_supports_tpm2_native: >-
|
||||
{{
|
||||
_initramfs_generator == 'mkinitcpio'
|
||||
or ('tpm2-tss' in (_dracut_modules_check.stdout | default('')))
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_tpm2_method: "{{ 'systemd-cryptenroll' if _supports_tpm2_native | bool else 'clevis' }}"
|
||||
|
||||
# --- Auto-upgrade to dracut when tpm2-tss available but generator isn't dracut ---
|
||||
- name: Switch to dracut for TPM2 support
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method == 'systemd-cryptenroll'
|
||||
- _initramfs_generator not in ['dracut', 'mkinitcpio']
|
||||
vars:
|
||||
_dracut_cmd: >-
|
||||
bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"
|
||||
ansible.builtin.set_fact:
|
||||
_initramfs_generator: dracut
|
||||
_configuration_platform: >-
|
||||
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
|
||||
|
||||
- name: Report TPM2 configuration
|
||||
when: configuration_luks_auto_method == 'tpm2'
|
||||
ansible.builtin.debug:
|
||||
msg: "TPM2 unlock: {{ _tpm2_method | default('none') }} | initramfs: {{ _initramfs_generator }}"
|
||||
@@ -107,7 +107,7 @@
|
||||
when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
LUKS keyfile enrollment failed - falling back to manual unlock at boot.
|
||||
LUKS keyfile enrollment failed — falling back to manual unlock at boot.
|
||||
The system will prompt for the LUKS passphrase during startup.
|
||||
|
||||
- name: Fallback to manual LUKS unlock if keyfile enrollment failed
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
---
|
||||
# TPM2 enrollment via systemd-cryptenroll.
|
||||
# Works with dracut and mkinitcpio (sd-encrypt). The user-set passphrase
|
||||
# remains as a backup unlock method - no auto-generated keyfiles.
|
||||
- name: Enroll TPM2 for LUKS
|
||||
block:
|
||||
# Tempfile in chroot /tmp — accessible by both chroot and host commands
|
||||
- name: Create temporary passphrase file for TPM2 enrollment
|
||||
ansible.builtin.tempfile:
|
||||
path: /mnt/root
|
||||
path: /mnt/tmp
|
||||
prefix: luks-passphrase-
|
||||
state: file
|
||||
register: _tpm2_passphrase_tempfile
|
||||
register: configuration_luks_tpm2_passphrase_tempfile
|
||||
|
||||
- name: Write passphrase into temporary file
|
||||
- name: Write passphrase into temporary file for TPM2 enrollment
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ _tpm2_passphrase_tempfile.path }}"
|
||||
dest: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
|
||||
content: "{{ configuration_luks_passphrase }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Ensure TPM device is accessible in chroot
|
||||
ansible.builtin.shell: >-
|
||||
ls /mnt/dev/tpmrm0 2>/dev/null
|
||||
|| (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0)
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Enroll TPM2 token via systemd-cryptenroll
|
||||
- name: Enroll TPM2 token
|
||||
vars:
|
||||
_enroll_args: >-
|
||||
configuration_luks_enroll_args: >-
|
||||
{{
|
||||
[
|
||||
'/usr/bin/systemd-cryptenroll',
|
||||
@@ -37,28 +28,69 @@
|
||||
'--tpm2-with-pin=false',
|
||||
'--wipe-slot=tpm2',
|
||||
'--unlock-key-file=' + (
|
||||
_tpm2_passphrase_tempfile.path | regex_replace('^/mnt', '')
|
||||
configuration_luks_tpm2_passphrase_tempfile.path
|
||||
| regex_replace('^/mnt', '')
|
||||
)
|
||||
]
|
||||
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||
if configuration_luks_tpm2_pcrs | length > 0 else [])
|
||||
+ [configuration_luks_device]
|
||||
}}
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ _enroll_args | join(' ') }}"
|
||||
register: _tpm2_enroll_result
|
||||
changed_when: _tpm2_enroll_result.rc == 0
|
||||
configuration_luks_enroll_chroot_cmd: >-
|
||||
{{ chroot_command }} {{ configuration_luks_enroll_args | join(' ') }}
|
||||
ansible.builtin.command: "{{ configuration_luks_enroll_chroot_cmd }}"
|
||||
register: configuration_luks_tpm2_enroll_chroot
|
||||
changed_when: configuration_luks_tpm2_enroll_chroot.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Retry TPM2 enrollment in installer environment
|
||||
when:
|
||||
- (configuration_luks_tpm2_enroll_chroot.rc | default(1)) != 0
|
||||
vars:
|
||||
configuration_luks_enroll_args: >-
|
||||
{{
|
||||
[
|
||||
'/usr/bin/systemd-cryptenroll',
|
||||
'--tpm2-device=' + configuration_luks_tpm2_device,
|
||||
'--tpm2-with-pin=false',
|
||||
'--wipe-slot=tpm2',
|
||||
'--unlock-key-file=' + configuration_luks_tpm2_passphrase_tempfile.path
|
||||
]
|
||||
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||
if configuration_luks_tpm2_pcrs | length > 0 else [])
|
||||
+ [configuration_luks_device]
|
||||
}}
|
||||
ansible.builtin.command:
|
||||
argv: "{{ configuration_luks_enroll_args }}"
|
||||
register: configuration_luks_tpm2_enroll_host
|
||||
changed_when: configuration_luks_tpm2_enroll_host.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Validate TPM2 enrollment succeeded
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
(configuration_luks_tpm2_enroll_chroot.rc | default(1)) == 0
|
||||
or (configuration_luks_tpm2_enroll_host.rc | default(1)) == 0
|
||||
fail_msg: >-
|
||||
TPM2 enrollment failed.
|
||||
chroot rc={{ configuration_luks_tpm2_enroll_chroot.rc | default('n/a') }},
|
||||
host rc={{ configuration_luks_tpm2_enroll_host.rc | default('n/a') }},
|
||||
chroot stderr={{ configuration_luks_tpm2_enroll_chroot.stderr | default('') }},
|
||||
host stderr={{ configuration_luks_tpm2_enroll_host.stderr | default('') }}
|
||||
rescue:
|
||||
- name: TPM2 enrollment failed
|
||||
- name: Warn about TPM2 enrollment failure
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
TPM2 enrollment failed: {{ _tpm2_enroll_result.stderr | default('unknown') }}.
|
||||
The system will require the passphrase for LUKS unlock on boot.
|
||||
TPM2 can be enrolled post-deployment via: systemd-cryptenroll --tpm2-device=auto {{ configuration_luks_device }}
|
||||
WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt.
|
||||
The system will use a keyfile instead of TPM2 for automatic LUKS unlock.
|
||||
|
||||
- name: Fallback to keyfile auto-decrypt
|
||||
ansible.builtin.set_fact:
|
||||
configuration_luks_auto_method: keyfile
|
||||
always:
|
||||
- name: Remove temporary passphrase file
|
||||
when: _tpm2_passphrase_tempfile.path is defined
|
||||
- name: Remove TPM2 enrollment passphrase file
|
||||
when: configuration_luks_tpm2_passphrase_tempfile.path is defined
|
||||
ansible.builtin.file:
|
||||
path: "{{ _tpm2_passphrase_tempfile.path }}"
|
||||
path: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
|
||||
state: absent
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
set smartindent
|
||||
set mouse=a
|
||||
insertafter: EOF
|
||||
marker: "\" {mark} CUSTOM VIM CONFIG"
|
||||
marker: "# {mark} CUSTOM VIM CONFIG"
|
||||
failed_when: false
|
||||
|
||||
# Tuned for VM workloads: low swappiness, aggressive writeback, large page-cluster
|
||||
@@ -30,6 +30,7 @@
|
||||
- name: Create zram config
|
||||
when:
|
||||
- (os != "debian" or (os_version | string) != "11") and os != "rhel"
|
||||
- os not in ["alpine", "void"]
|
||||
- system_cfg.features.swap.enabled | bool
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/zram-generator.conf
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
- name: Enable the firewall daemon in the install chroot
|
||||
when:
|
||||
- firewall_phase == 'install'
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ system_cfg.features.firewall.backend }}"
|
||||
register: _firewall_enable
|
||||
changed_when: _firewall_enable.rc == 0
|
||||
failed_when: >-
|
||||
_firewall_enable.rc != 0
|
||||
and 'No such file or directory' not in (_firewall_enable.stderr | default(''))
|
||||
and 'does not exist' not in (_firewall_enable.stderr | default(''))
|
||||
|
||||
# ufw's CLI needs a running kernel and is a no-op in the chroot (leaves ENABLED=no),
|
||||
# so its activation and SSH rule are applied here, after reboot.
|
||||
- name: Allow SSH through ufw before enabling
|
||||
when:
|
||||
- firewall_phase == 'postreboot'
|
||||
- system_cfg.features.firewall.backend == 'ufw'
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
- system_cfg.features.ssh.enabled | bool
|
||||
ansible.builtin.command: ufw allow 22/tcp
|
||||
register: _ufw_allow
|
||||
changed_when: "'added' in _ufw_allow.stdout or 'updated' in _ufw_allow.stdout"
|
||||
|
||||
- name: Activate ufw on the booted target
|
||||
when:
|
||||
- firewall_phase == 'postreboot'
|
||||
- system_cfg.features.firewall.backend == 'ufw'
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
ansible.builtin.command: ufw --force enable
|
||||
register: _ufw_enable
|
||||
changed_when: "'active' in _ufw_enable.stdout"
|
||||
@@ -26,7 +26,7 @@
|
||||
- name: Remove RHEL ISO fstab entry when not using local repo
|
||||
when:
|
||||
- os == "rhel"
|
||||
- system_cfg.content.source != "dvd"
|
||||
- system_cfg.features.rhel_repo.source != "iso"
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/fstab
|
||||
regexp: "^.*\\/dvd.*$"
|
||||
@@ -35,7 +35,7 @@
|
||||
- name: Replace ISO UUID entry with /dev/sr0 in fstab
|
||||
when:
|
||||
- os == "rhel"
|
||||
- system_cfg.content.source == "dvd"
|
||||
- system_cfg.features.rhel_repo.source == "iso"
|
||||
vars:
|
||||
configuration_fstab_dvd_line: >-
|
||||
{{
|
||||
@@ -53,7 +53,7 @@
|
||||
when:
|
||||
- os == "rhel"
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.content.source == "dvd"
|
||||
- system_cfg.features.rhel_repo.source == "iso"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- dd
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- regexp: ^GRUB_CMDLINE_LINUX_DEFAULT=
|
||||
line: 'GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3{{ (" " ~ (_hardware_profile_kernel_params | join(" "))) if (_hardware_profile_kernel_params | default([]) | length > 0) else "" }}"'
|
||||
line: GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3"
|
||||
- regexp: ^GRUB_TIMEOUT=
|
||||
line: GRUB_TIMEOUT=1
|
||||
loop_control:
|
||||
@@ -43,21 +43,19 @@
|
||||
}}
|
||||
grub_root_flags: >-
|
||||
{{ ['rootflags=subvol=@'] if system_cfg.filesystem == 'btrfs' else [] }}
|
||||
# String-concat (not list-concat like grub_kernel_cmdline_base below): ansible-lint's
|
||||
# jinja render trips on list+list when grub_lvm_args leads the expression here.
|
||||
grub_cmdline_linux_base: >-
|
||||
{{
|
||||
((grub_lvm_args | join(' ')) ~ ' ' ~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
|
||||
(['crashkernel=auto'] + grub_lvm_args)
|
||||
| join(' ')
|
||||
}}
|
||||
grub_kernel_cmdline_base: >-
|
||||
{{
|
||||
(
|
||||
(['root=UUID=' + grub_root_uuid]
|
||||
if grub_root_uuid | length > 0 else [])
|
||||
+ ['ro']
|
||||
+ ['ro', 'crashkernel=auto']
|
||||
+ grub_lvm_args
|
||||
+ grub_root_flags
|
||||
+ (_hardware_profile_kernel_params | default([]))
|
||||
)
|
||||
| join(' ')
|
||||
}}
|
||||
|
||||
@@ -5,22 +5,18 @@
|
||||
- name: Include configuration tasks
|
||||
when: configuration_task.when | default(true)
|
||||
ansible.builtin.include_tasks: "{{ configuration_task.file }}"
|
||||
vars:
|
||||
firewall_phase: install
|
||||
loop:
|
||||
- file: repositories.yml
|
||||
when: "{{ os_family == 'Debian' }}"
|
||||
- file: banner.yml
|
||||
- file: fstab.yml
|
||||
- file: locales.yml
|
||||
- file: ssh.yml
|
||||
- file: services.yml
|
||||
- file: firewall.yml
|
||||
- file: grub.yml
|
||||
- file: encryption.yml
|
||||
when: "{{ system_cfg.luks.enabled | bool }}"
|
||||
- file: bootloader.yml
|
||||
- file: secure_boot.yml
|
||||
when: "{{ system_cfg.features.secure_boot.enabled | bool }}"
|
||||
- file: extras.yml
|
||||
- file: network.yml
|
||||
- file: users.yml
|
||||
|
||||
@@ -1,51 +1,38 @@
|
||||
---
|
||||
- name: Read network interfaces
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ip
|
||||
- -o
|
||||
- link
|
||||
- show
|
||||
register: configuration_ip_link
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Detect available network interface names
|
||||
vars:
|
||||
configuration_detected_interfaces: >-
|
||||
{{
|
||||
configuration_ip_link.stdout
|
||||
| default('')
|
||||
| regex_findall('^[0-9]+: ([^:]+):', multiline=True)
|
||||
| reject('equalto', 'lo')
|
||||
| list
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_detected_interfaces: "{{ configuration_detected_interfaces }}"
|
||||
|
||||
- name: Validate at least one network interface detected
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- configuration_detected_interfaces | length > 0
|
||||
fail_msg: Failed to detect any network interfaces.
|
||||
|
||||
- name: Set DNS configuration facts
|
||||
ansible.builtin.set_fact:
|
||||
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
|
||||
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
|
||||
|
||||
# 2+ unnamed interfaces would all match the first-ethernet glob, leaving the rest unconfigured.
|
||||
- name: Require an explicit name on every interface for multi-NIC
|
||||
vars:
|
||||
_unnamed: "{{ system_cfg.network.interfaces | map(attribute='name', default='') | map('string') | select('equalto', '') | list | length }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.network.interfaces | length <= 1 or _unnamed == 0
|
||||
fail_msg: >-
|
||||
Multi-NIC (system.network.interfaces with 2+ entries) requires a name on
|
||||
every interface; the first-adapter glob only binds a single NIC.
|
||||
|
||||
# Probe /mnt to detect the stack the installed rootfs will run (nothing runs in
|
||||
# the chroot). NM is checked first and wins, since bootstrap installs it on every
|
||||
# family; the rest are the fallback for a non-NM base image.
|
||||
- name: Probe the installed network stack on the target rootfs
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
register: configuration_net_probe
|
||||
loop:
|
||||
- /mnt/usr/bin/nmcli
|
||||
- /mnt/usr/lib/systemd/system/NetworkManager.service
|
||||
- /mnt/usr/sbin/netplan
|
||||
- /mnt/etc/netplan
|
||||
- /mnt/sbin/ifup
|
||||
- /mnt/usr/sbin/ifup
|
||||
- /mnt/etc/systemd/system/multi-user.target.wants/systemd-networkd.service
|
||||
- /mnt/etc/systemd/system/dbus-org.freedesktop.network1.service
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Resolve the network backend from the probe
|
||||
vars:
|
||||
_found: "{{ configuration_net_probe.results | selectattr('stat.exists') | map(attribute='item') | list }}"
|
||||
ansible.builtin.set_fact:
|
||||
configuration_network_backend: >-
|
||||
{{
|
||||
'nm' if (['/mnt/usr/bin/nmcli', '/mnt/usr/lib/systemd/system/NetworkManager.service'] | intersect(_found))
|
||||
else 'netplan' if (['/mnt/usr/sbin/netplan', '/mnt/etc/netplan'] | intersect(_found))
|
||||
else 'eni' if (['/mnt/sbin/ifup', '/mnt/usr/sbin/ifup'] | intersect(_found))
|
||||
else 'networkd' if (['/mnt/etc/systemd/system/multi-user.target.wants/systemd-networkd.service', '/mnt/etc/systemd/system/dbus-org.freedesktop.network1.service'] | intersect(_found))
|
||||
else 'nm'
|
||||
}}
|
||||
|
||||
- name: Configure networking for the detected backend {{ configuration_network_backend }}
|
||||
ansible.builtin.include_tasks: "network_{{ configuration_network_backend }}.yml"
|
||||
- name: Configure networking
|
||||
ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"
|
||||
|
||||
36
roles/configuration/tasks/network_alpine.yml
Normal file
36
roles/configuration/tasks/network_alpine.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
- name: Write Alpine network interfaces
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/network/interfaces
|
||||
mode: "0644"
|
||||
content: |
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
{% set inv_name = iface.name | default('') | string %}
|
||||
{% set det_name = configuration_detected_interfaces[loop.index0] | default('eth' ~ loop.index0) %}
|
||||
{% set iface_name = inv_name if inv_name | length > 0 else det_name %}
|
||||
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
|
||||
|
||||
auto {{ iface_name }}
|
||||
iface {{ iface_name }} inet {{ 'static' if has_static else 'dhcp' }}
|
||||
{% if has_static %}
|
||||
address {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
gateway {{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
- name: Set Alpine DNS resolvers
|
||||
when: configuration_dns_list | length > 0 or configuration_dns_search | length > 0
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/resolv.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
{% if configuration_dns_search | length > 0 %}
|
||||
search {{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% for resolver in configuration_dns_list %}
|
||||
nameserver {{ resolver }}
|
||||
{% endfor %}
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
# ifupdown can't glob iface stanzas (no mapping on ifupdown2/Proxmox), so ENI binds
|
||||
# a literal name detected here. The chroot only sees live-ISO names: on a real
|
||||
# ifupdown base, set system.network.interfaces[].name to the installed name. Bootstrap
|
||||
# installs NetworkManager, so this fires only on a non-NM base image.
|
||||
- name: Detect ethernet interface names
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ip
|
||||
- -o
|
||||
- link
|
||||
- show
|
||||
register: configuration_eni_link
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Resolve detected ethernet interface names
|
||||
ansible.builtin.set_fact:
|
||||
configuration_eni_detected: >-
|
||||
{{
|
||||
configuration_eni_link.stdout | default('')
|
||||
| regex_findall('^[0-9]+: ([^:@]+)[@:].*?link/ether', multiline=True)
|
||||
}}
|
||||
|
||||
- name: Ensure the network configuration directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/network
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write the ifupdown interfaces file
|
||||
ansible.builtin.template:
|
||||
src: network_eni.j2
|
||||
dest: /mnt/etc/network/interfaces
|
||||
mode: "0644"
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
- name: Ensure the netplan directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/netplan
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write the netplan configuration
|
||||
ansible.builtin.template:
|
||||
src: network_netplan.j2
|
||||
dest: /mnt/etc/netplan/10-sg.yaml
|
||||
mode: "0600"
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
- name: Ensure the systemd-networkd directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/systemd/network
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write systemd-networkd configuration per interface
|
||||
vars:
|
||||
configuration_iface: "{{ item }}"
|
||||
ansible.builtin.template:
|
||||
src: network_networkd.j2
|
||||
dest: "/mnt/etc/systemd/network/10-static-{{ idx }}.network"
|
||||
mode: "0644"
|
||||
loop: "{{ system_cfg.network.interfaces }}"
|
||||
loop_control:
|
||||
index_var: idx
|
||||
label: "10-static-{{ idx }}"
|
||||
@@ -2,6 +2,7 @@
|
||||
- name: Copy NetworkManager keyfile per interface
|
||||
vars:
|
||||
configuration_iface: "{{ item }}"
|
||||
configuration_iface_name: "{{ item.name | default(configuration_detected_interfaces[idx] | default('')) }}"
|
||||
configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}"
|
||||
ansible.builtin.template:
|
||||
src: network.j2
|
||||
|
||||
26
roles/configuration/tasks/network_void.yml
Normal file
26
roles/configuration/tasks/network_void.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
- name: Write dhcpcd configuration
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/dhcpcd.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
{% set inv_name = iface.name | default('') | string %}
|
||||
{% set det_name = configuration_detected_interfaces[loop.index0] | default('eth' ~ loop.index0) %}
|
||||
{% set iface_name = inv_name if inv_name | length > 0 else det_name %}
|
||||
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
|
||||
{% if has_static %}
|
||||
interface {{ iface_name }}
|
||||
static ip_address={{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
static routers={{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_list | length > 0 %}
|
||||
static domain_name_servers={{ configuration_dns_list | join(' ') }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_search | length > 0 %}
|
||||
static domain_search={{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -1,84 +1,25 @@
|
||||
---
|
||||
# Config runs against the chroot, so these write /mnt directly via templates
|
||||
# rather than apt_repository/yum_repository, which would touch the live host.
|
||||
- name: Write the apt sources.list
|
||||
when: os_family == 'Debian'
|
||||
- name: Write final sources.list
|
||||
vars:
|
||||
_debian_release_map:
|
||||
"10": buster
|
||||
"11": bullseye
|
||||
"12": bookworm
|
||||
"13": trixie
|
||||
unstable: sid
|
||||
_ubuntu_release_map:
|
||||
ubuntu: questing
|
||||
ubuntu-lts: resolute
|
||||
ubuntu-lts: noble
|
||||
ansible.builtin.template:
|
||||
src: "{{ os | replace('-lts', '') }}.sources.list.j2"
|
||||
dest: /mnt/etc/apt/sources.list
|
||||
mode: "0644"
|
||||
|
||||
- name: Ensure apt performance and content-proxy configuration
|
||||
when: os_family == 'Debian'
|
||||
- name: Ensure apt performance configuration persists
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/apt/apt.conf.d/99performance
|
||||
content: |
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
Acquire::http::Proxy "{{ system_cfg.content.proxy }}";
|
||||
Acquire::https::Proxy "{{ system_cfg.content.proxy }}";
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Drop the install-time DVD repo from the target on non-dvd sources
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source != 'dvd'
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/yum.repos.d/redhat.repo
|
||||
state: absent
|
||||
|
||||
- name: Write the EL mirror repo on the target
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length > 0
|
||||
ansible.builtin.template:
|
||||
src: el_mirror.repo.j2
|
||||
dest: "/mnt/etc/yum.repos.d/{{ os }}.repo"
|
||||
mode: "0644"
|
||||
|
||||
- name: Find the stock vendor repos shipped by the release package
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length > 0
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/etc/yum.repos.d
|
||||
patterns: "*.repo"
|
||||
excludes: "{{ os }}.repo"
|
||||
register: el_stock_repos
|
||||
|
||||
- name: Remove the stock vendor repos so only the custom mirror is reachable
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length > 0
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ el_stock_repos.files | default([]) }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: Configure the dnf content proxy on the target
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.proxy | length > 0
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/dnf/dnf.conf
|
||||
line: "proxy={{ system_cfg.content.proxy }}"
|
||||
regexp: "^proxy="
|
||||
create: true
|
||||
mode: "0644"
|
||||
state: present
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
# Invoked post-reboot on the booted host, not in the chroot: subscription-manager
|
||||
# needs a running systemd and the live network.
|
||||
- name: Add the Satellite host to /etc/hosts
|
||||
when: system_cfg.content.satellite.ip | length > 0
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
line: "{{ system_cfg.content.satellite.ip }} {{ system_cfg.content.satellite.host }}"
|
||||
regexp: "[[:space:]]{{ system_cfg.content.satellite.host | regex_escape }}([[:space:]]|$)"
|
||||
state: present
|
||||
|
||||
- name: Fetch the Katello CA consumer RPM
|
||||
ansible.builtin.get_url:
|
||||
url: >-
|
||||
{{ system_cfg.content.satellite.ca_url
|
||||
if (system_cfg.content.satellite.ca_url | length > 0)
|
||||
else 'https://' ~ system_cfg.content.satellite.host ~ '/pub/katello-ca-consumer-latest.noarch.rpm' }}
|
||||
dest: /tmp/katello-ca-consumer-latest.noarch.rpm
|
||||
validate_certs: false
|
||||
mode: "0644"
|
||||
|
||||
- name: Install the Katello CA consumer RPM
|
||||
ansible.builtin.dnf:
|
||||
name: /tmp/katello-ca-consumer-latest.noarch.rpm
|
||||
state: present
|
||||
disable_gpg_check: true
|
||||
|
||||
- name: Clean any stale subscription identity
|
||||
ansible.builtin.command: subscription-manager clean
|
||||
changed_when: true
|
||||
|
||||
- name: Register with Satellite via activation key
|
||||
no_log: true
|
||||
community.general.redhat_subscription:
|
||||
state: present
|
||||
server_hostname: "{{ system_cfg.content.satellite.host }}"
|
||||
org_id: "{{ system_cfg.content.satellite.org }}"
|
||||
activationkey: "{{ system_cfg.content.satellite.activation_key }}"
|
||||
environment: "{{ system_cfg.content.satellite.environment | default(omit, true) }}"
|
||||
force_register: true
|
||||
server_proxy_hostname: "{{ (system_cfg.content.proxy | urlsplit('hostname')) | default(omit, true) }}"
|
||||
server_proxy_port: "{{ (system_cfg.content.proxy | urlsplit('port')) | default(omit, true) }}"
|
||||
syspurpose:
|
||||
service_level_agreement: "{{ system_cfg.content.satellite.service_level | default(omit, true) }}"
|
||||
sync: true
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
- name: Validate Secure Boot is supported on this OS
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- os in ['archlinux', 'debian', 'ubuntu', 'ubuntu-lts',
|
||||
'rhel', 'rocky', 'almalinux', 'fedora']
|
||||
fail_msg: >-
|
||||
Secure Boot is not supported on {{ os }} in this bootstrap. Supported:
|
||||
Arch (sbctl) and Debian/Ubuntu/RHEL/Rocky/Alma/Fedora (shim). Disable
|
||||
system.features.secure_boot.enabled or pick a supported OS.
|
||||
quiet: true
|
||||
|
||||
- name: Configure shim-based Secure Boot
|
||||
when: os != 'archlinux'
|
||||
ansible.builtin.include_tasks: secure_boot/shim.yml
|
||||
|
||||
- name: Configure sbctl Secure Boot
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.include_tasks: secure_boot/sbctl.yml
|
||||
@@ -1,115 +0,0 @@
|
||||
---
|
||||
- name: Configure sbctl Secure Boot
|
||||
block:
|
||||
- name: Create Secure Boot signing keys
|
||||
ansible.builtin.command: "{{ chroot_command }} sbctl create-keys"
|
||||
register: _sbctl_create_keys
|
||||
changed_when: _sbctl_create_keys.rc == 0
|
||||
failed_when:
|
||||
- _sbctl_create_keys.rc != 0
|
||||
- "'already exists' not in (_sbctl_create_keys.stderr | default(''))"
|
||||
|
||||
- name: Enroll Secure Boot keys in firmware
|
||||
ansible.builtin.command: "{{ chroot_command }} sbctl enroll-keys --microsoft"
|
||||
register: _sbctl_enroll
|
||||
changed_when: _sbctl_enroll.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Install first-boot enrollment service if chroot enrollment failed
|
||||
when: _sbctl_enroll.rc | default(1) != 0
|
||||
block:
|
||||
- name: Create first-boot sbctl enrollment service
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/system/sbctl-enroll.service
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Enroll Secure Boot keys via sbctl
|
||||
ConditionPathExists=!/var/lib/sbctl/.enrolled
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/sbctl enroll-keys --microsoft
|
||||
ExecStartPost=/usr/bin/touch /var/lib/sbctl/.enrolled
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
- name: Enable first-boot enrollment service
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable sbctl-enroll.service"
|
||||
register: _sbctl_service_enable
|
||||
changed_when: _sbctl_service_enable.rc == 0
|
||||
|
||||
- name: Find kernel images to sign
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/boot
|
||||
patterns: "vmlinuz-*"
|
||||
file_type: file
|
||||
register: _sbctl_kernel_images
|
||||
|
||||
- name: Sign kernel images
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} sbctl sign -s {{ item.path | regex_replace('^/mnt', '') }}
|
||||
loop: "{{ _sbctl_kernel_images.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
register: _sbctl_sign_kernel
|
||||
changed_when: _sbctl_sign_kernel.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Sign GRUB EFI binary
|
||||
vars:
|
||||
_grub_efi_path: "{{ partitioning_efi_mountpoint }}/EFI/archlinux/grubx64.efi"
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} sbctl sign -s {{ _grub_efi_path }}
|
||||
register: _sbctl_sign_grub
|
||||
changed_when: _sbctl_sign_grub.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Ensure pacman hooks directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/pacman.d/hooks
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Install sbctl auto-signing pacman hook
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/pacman.d/hooks/99-sbctl-sign.hook
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Trigger]
|
||||
Operation = Install
|
||||
Operation = Upgrade
|
||||
Type = Path
|
||||
Target = boot/vmlinuz-*
|
||||
Target = usr/lib/modules/*/vmlinuz
|
||||
|
||||
[Action]
|
||||
Description = Signing kernel images for Secure Boot...
|
||||
When = PostTransaction
|
||||
Exec = /usr/bin/sbctl sign-all
|
||||
Depends = sbctl
|
||||
|
||||
- name: Verify sbctl signing status
|
||||
ansible.builtin.command: "{{ chroot_command }} sbctl verify"
|
||||
register: _sbctl_verify
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Report sbctl Secure Boot status
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Secure Boot (sbctl):
|
||||
Enrollment={{ 'done' if (_sbctl_enroll.rc | default(1)) == 0 else 'deferred to first boot' }}.
|
||||
{{ _sbctl_verify.stdout | default('Verify not available') }}
|
||||
|
||||
rescue:
|
||||
- name: Secure Boot setup failed
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
sbctl Secure Boot setup failed.
|
||||
On VMs make sure the OVMF firmware is in Setup Mode (fresh NVRAM).
|
||||
On bare metal enter the firmware setup and switch to Setup Mode first.
|
||||
To recover manually: sbctl create-keys && sbctl enroll-keys --microsoft && sbctl sign-all
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
- name: Configure shim-based Secure Boot
|
||||
vars:
|
||||
_efi_vendor: >-
|
||||
{{
|
||||
"redhat" if os == "rhel"
|
||||
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
|
||||
}}
|
||||
block:
|
||||
- name: Find shim binary in target system
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail &&
|
||||
{{ chroot_command }} find /usr/lib/shim /boot/efi/EFI
|
||||
\( -name 'shimx64.efi.signed.latest' -o -name 'shimx64.efi.dualsigned'
|
||||
-o -name 'shimx64.efi.signed' -o -name 'shimx64.efi' \)
|
||||
-type f | sort -r | head -1
|
||||
executable: /bin/bash
|
||||
register: _shim_find_result
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Copy shim to EFI vendor directory
|
||||
when:
|
||||
- _shim_find_result.stdout | default('') | length > 0
|
||||
- _configuration_platform.grub_install | bool
|
||||
ansible.builtin.command: >-
|
||||
cp /mnt{{ _shim_find_result.stdout_lines | first }}
|
||||
/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi
|
||||
register: _shim_copy_result
|
||||
changed_when: _shim_copy_result.rc == 0
|
||||
|
||||
- name: Verify shim is present
|
||||
ansible.builtin.stat:
|
||||
path: "/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi"
|
||||
register: _shim_stat
|
||||
|
||||
- name: Report Secure Boot status
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Secure Boot (shim): {{
|
||||
'shimx64.efi installed at ' ~ partitioning_efi_mountpoint ~ '/EFI/' ~ _efi_vendor
|
||||
if (_shim_stat.stat.exists | default(false))
|
||||
else 'shimx64.efi not found, shim package may handle placement on first boot'
|
||||
}}
|
||||
@@ -11,16 +11,6 @@
|
||||
register: configuration_setfiles_result
|
||||
changed_when: configuration_setfiles_result.rc == 0
|
||||
|
||||
# setfiles in the chroot misses paths created at first boot (e.g. /var/lib/sss),
|
||||
# leaving unlabeled_t files that block services under enforcing SELinux. Force a
|
||||
# complete relabel on first boot; fixfiles consumes and removes the flag.
|
||||
- name: Force a complete SELinux relabel on first boot
|
||||
when: os in ['almalinux', 'rocky', 'rhel'] and system_cfg.features.selinux.enabled | bool
|
||||
ansible.builtin.file:
|
||||
path: /mnt/.autorelabel
|
||||
state: touch
|
||||
mode: "0644"
|
||||
|
||||
# Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux
|
||||
# is left permissive and expected to relabel on first boot.
|
||||
- name: Disable SELinux
|
||||
|
||||
@@ -1,248 +1,80 @@
|
||||
---
|
||||
- name: Resolve desktop facts
|
||||
when: system_cfg.features.desktop.enabled | bool
|
||||
vars:
|
||||
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
|
||||
ansible.builtin.set_fact:
|
||||
# KDE resolves to the plasmalogin unit on Arch/Fedora44+ (Plasma 6.6), else sddm.
|
||||
_desktop_dm: >-
|
||||
{{
|
||||
('plasmalogin'
|
||||
if system_cfg.features.desktop.display_manager == 'plasma-login-manager'
|
||||
else system_cfg.features.desktop.display_manager)
|
||||
if (system_cfg.features.desktop.display_manager | length > 0)
|
||||
else (
|
||||
('plasmalogin'
|
||||
if (os == 'archlinux' or (os == 'fedora' and (os_version | int) >= 44))
|
||||
else 'sddm')
|
||||
if system_cfg.features.desktop.environment == 'kde'
|
||||
else (configuration_desktop_dm_map[system_cfg.features.desktop.environment] | default(''))
|
||||
)
|
||||
}}
|
||||
_desktop_session: "{{ system_cfg.features.desktop.session | default('') }}"
|
||||
# Explicit session wins, else the per-environment command. Single source of
|
||||
# truth for the greetd assert, the config gate, and the template.
|
||||
_greetd_session: >-
|
||||
{{
|
||||
system_cfg.features.desktop.session
|
||||
if (system_cfg.features.desktop.session | default('') | length > 0)
|
||||
else (configuration_desktop_session_cmd_map[system_cfg.features.desktop.environment] | default(''))
|
||||
}}
|
||||
_desktop_autologin_user: >-
|
||||
{{
|
||||
_autologin
|
||||
if (_autologin | string | lower not in ['', 'false'] and _autologin in system_cfg.users)
|
||||
else ''
|
||||
}}
|
||||
|
||||
- name: Enable systemd services
|
||||
when: _configuration_platform.init_system == 'systemd'
|
||||
vars:
|
||||
configuration_systemd_services: >-
|
||||
{{
|
||||
['NetworkManager', _configuration_platform.time_sync_service]
|
||||
['NetworkManager']
|
||||
+ (['firewalld'] if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ (['logrotate'] if os == 'archlinux' else [])
|
||||
+ (['bluetooth'] if system_cfg.features.desktop.enabled | bool else [])
|
||||
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else [])
|
||||
}}
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}"
|
||||
loop: "{{ configuration_systemd_services }}"
|
||||
register: configuration_enable_service_result
|
||||
changed_when: configuration_enable_service_result.rc == 0
|
||||
failed_when: >-
|
||||
configuration_enable_service_result.rc != 0
|
||||
and 'No such file or directory' not in (configuration_enable_service_result.stderr | default(''))
|
||||
and 'does not exist' not in (configuration_enable_service_result.stderr | default(''))
|
||||
|
||||
- name: Check for the EL qemu-guest-agent RPC allow-list
|
||||
ansible.builtin.stat:
|
||||
path: /mnt/etc/sysconfig/qemu-ga
|
||||
register: configuration_qga_sysconfig
|
||||
|
||||
- name: Allow clone-stamping RPCs in the EL qemu-guest-agent allow-list
|
||||
when: configuration_qga_sysconfig.stat.exists
|
||||
ansible.builtin.replace:
|
||||
path: /mnt/etc/sysconfig/qemu-ga
|
||||
regexp: '^(FILTER_RPC_ARGS="--allow-rpcs=(?:(?!guest-exec)[^"])*)"'
|
||||
replace: '\1,guest-exec,guest-exec-status,guest-file-open,guest-file-close,guest-file-read,guest-file-write"'
|
||||
|
||||
- name: Enable display manager for selected desktop
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm | length > 0
|
||||
- _desktop_dm != 'ly'
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ _desktop_dm }}"
|
||||
register: configuration_enable_dm_result
|
||||
changed_when: configuration_enable_dm_result.rc == 0
|
||||
# Unlike optional services above, a missing/unenabled DM is fatal: chroot
|
||||
# systemctl can exit 0 while only warning on stderr, so check both.
|
||||
failed_when: >-
|
||||
configuration_enable_dm_result.rc != 0
|
||||
or 'No such file or directory' in (configuration_enable_dm_result.stderr | default(''))
|
||||
or 'does not exist' in (configuration_enable_dm_result.stderr | default(''))
|
||||
|
||||
- name: Enable ly on its tty
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'ly'
|
||||
- name: Enable OpenRC services
|
||||
when: _configuration_platform.init_system == 'openrc'
|
||||
vars:
|
||||
_ly_tty: tty2
|
||||
configuration_openrc_services: >-
|
||||
{{
|
||||
['networking']
|
||||
+ (['sshd'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ ([system_cfg.features.firewall.backend] if system_cfg.features.firewall.enabled | bool else [])
|
||||
}}
|
||||
block:
|
||||
- name: Enable ly display manager
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable ly@{{ _ly_tty }}.service"
|
||||
register: configuration_enable_ly_result
|
||||
changed_when: configuration_enable_ly_result.rc == 0
|
||||
failed_when: >-
|
||||
configuration_enable_ly_result.rc != 0
|
||||
or 'No such file or directory' in (configuration_enable_ly_result.stderr | default(''))
|
||||
or 'does not exist' in (configuration_enable_ly_result.stderr | default(''))
|
||||
|
||||
# ly drives the VT itself; mask getty so logind never spawns a login on that tty.
|
||||
- name: Mask getty on ly's tty
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl mask getty@{{ _ly_tty }}.service"
|
||||
register: configuration_mask_getty_result
|
||||
changed_when: configuration_mask_getty_result.rc == 0
|
||||
failed_when: >-
|
||||
configuration_mask_getty_result.rc != 0
|
||||
and 'No such file or directory' not in (configuration_mask_getty_result.stderr | default(''))
|
||||
and 'does not exist' not in (configuration_mask_getty_result.stderr | default(''))
|
||||
|
||||
- name: Set default systemd target
|
||||
when: _configuration_platform.init_system == 'systemd'
|
||||
vars:
|
||||
_default_target: "{{ 'graphical.target' if system_cfg.features.desktop.enabled | bool else 'multi-user.target' }}"
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl set-default {{ _default_target }}"
|
||||
register: _set_default_target_result
|
||||
changed_when: _set_default_target_result.rc == 0
|
||||
|
||||
- name: Enable PipeWire user services globally
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl --global enable {{ item }}"
|
||||
loop: "{{ configuration_desktop_audio_units }}"
|
||||
register: _desktop_audio_result
|
||||
changed_when: _desktop_audio_result.rc == 0
|
||||
failed_when: >-
|
||||
_desktop_audio_result.rc != 0
|
||||
and 'No such file or directory' not in (_desktop_audio_result.stderr | default(''))
|
||||
and 'does not exist' not in (_desktop_audio_result.stderr | default(''))
|
||||
|
||||
- name: Assert greetd has a real session command to launch
|
||||
when:
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'greetd'
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _greetd_session | length > 0
|
||||
- not (_greetd_session | trim | regex_search('\\.desktop$'))
|
||||
fail_msg: >-
|
||||
greetd needs an executable session command, but the resolved command for desktop
|
||||
environment '{{ system_cfg.features.desktop.environment }}' is
|
||||
'{{ _greetd_session }}'. greetd suits wlroots compositors (sway, hyprland) that
|
||||
launch from a plain command; kde/gnome ship a '.desktop' session and should use
|
||||
their own display manager (sddm, gdm). Set features.desktop.session to an
|
||||
executable, or pick a different display manager.
|
||||
|
||||
- name: Generate greetd configuration
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'greetd'
|
||||
- _greetd_session | length > 0
|
||||
block:
|
||||
- name: Ensure greetd config directory exists
|
||||
- name: Ensure OpenRC runlevel directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/greetd
|
||||
path: /mnt/etc/runlevels/default
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write greetd config.toml
|
||||
ansible.builtin.template:
|
||||
src: greetd-config.toml.j2
|
||||
dest: /mnt/etc/greetd/config.toml
|
||||
mode: "0644"
|
||||
- name: Check OpenRC init scripts
|
||||
ansible.builtin.stat:
|
||||
path: "/mnt/etc/init.d/{{ item }}"
|
||||
loop: "{{ configuration_openrc_services }}"
|
||||
register: configuration_openrc_service_stats
|
||||
|
||||
- name: Configure GDM autologin
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'gdm'
|
||||
- _desktop_autologin_user | length > 0
|
||||
vars:
|
||||
# Debian gdm3 reads daemon.conf; RedHat/Arch gdm read custom.conf.
|
||||
_gdm_dir: "/mnt/etc/{{ 'gdm3' if os_family == 'Debian' else 'gdm' }}"
|
||||
_gdm_conf: "{{ 'daemon.conf' if os_family == 'Debian' else 'custom.conf' }}"
|
||||
block:
|
||||
- name: Ensure GDM config directory exists
|
||||
- name: Enable OpenRC services
|
||||
ansible.builtin.file:
|
||||
path: "{{ _gdm_dir }}"
|
||||
src: "/mnt/etc/init.d/{{ item.item }}"
|
||||
dest: "/mnt/etc/runlevels/default/{{ item.item }}"
|
||||
state: link
|
||||
loop: "{{ configuration_openrc_service_stats.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: item.stat.exists
|
||||
|
||||
- name: Enable runit services
|
||||
when: _configuration_platform.init_system == 'runit'
|
||||
vars:
|
||||
configuration_runit_services: >-
|
||||
{{
|
||||
['dhcpcd']
|
||||
+ (['sshd'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ ([system_cfg.features.firewall.backend] if system_cfg.features.firewall.enabled | bool else [])
|
||||
}}
|
||||
block:
|
||||
- name: Ensure runit service directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/var/service
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write GDM autologin config
|
||||
ansible.builtin.template:
|
||||
src: gdm-custom.conf.j2
|
||||
dest: "{{ _gdm_dir }}/{{ _gdm_conf }}"
|
||||
mode: "0644"
|
||||
- name: Check runit service definitions
|
||||
ansible.builtin.stat:
|
||||
path: "/mnt/etc/sv/{{ item }}"
|
||||
loop: "{{ configuration_runit_services }}"
|
||||
register: configuration_runit_service_stats
|
||||
|
||||
# SDDM and plasma-login-manager share the [Autologin] format and the KDE Wayland
|
||||
# session; only the config dir differs (sddm.conf.d vs plasmalogin.conf.d).
|
||||
- name: Configure SDDM / plasma-login-manager autologin
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm in ['sddm', 'plasmalogin']
|
||||
- _desktop_autologin_user | length > 0
|
||||
vars:
|
||||
_autologin_conf_dir: "/mnt/etc/{{ 'plasmalogin.conf.d' if _desktop_dm == 'plasmalogin' else 'sddm.conf.d' }}"
|
||||
block:
|
||||
- name: Ensure KDE login-manager config directory exists
|
||||
- name: Enable runit services
|
||||
ansible.builtin.file:
|
||||
path: "{{ _autologin_conf_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
# Plasma 6 ships the Wayland session as plasma.desktop; Plasma 5 ships it as
|
||||
# plasmawayland.desktop (plasma.desktop is the X11 session there). Pick the
|
||||
# installed Wayland session so autologin never lands on X11.
|
||||
- name: Discover installed KDE Wayland sessions
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/usr/share/wayland-sessions
|
||||
patterns: "plasma.desktop,plasmawayland.desktop"
|
||||
register: _kde_wayland_sessions
|
||||
|
||||
- name: Resolve the KDE Wayland session file
|
||||
ansible.builtin.set_fact:
|
||||
_sddm_session: >-
|
||||
{%- set names = _kde_wayland_sessions.files | map(attribute='path') | map('basename') | list -%}
|
||||
{{ 'plasma.desktop' if 'plasma.desktop' in names else (names | first | default('')) }}
|
||||
|
||||
- name: Write KDE login-manager autologin drop-in
|
||||
ansible.builtin.template:
|
||||
src: sddm-autologin.conf.j2
|
||||
dest: "{{ _autologin_conf_dir }}/10-autologin.conf"
|
||||
mode: "0644"
|
||||
|
||||
# ly ships a flat (sectionless) config.ini; edit it in place to keep upstream
|
||||
# defaults. Both keys are required: an unresolved session writes 'null', which
|
||||
# disables autologin rather than leaving it half-configured.
|
||||
- name: Configure ly autologin
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'ly'
|
||||
- _desktop_autologin_user | length > 0
|
||||
community.general.ini_file:
|
||||
path: /mnt/etc/ly/config.ini
|
||||
option: "{{ item.key }}"
|
||||
value: "{{ item.value }}"
|
||||
create: false
|
||||
mode: "0644"
|
||||
loop:
|
||||
- key: auto_login_user
|
||||
value: "{{ _desktop_autologin_user }}"
|
||||
- key: auto_login_session
|
||||
value: "{{ _greetd_session if (_greetd_session | length > 0) else 'null' }}"
|
||||
src: "/mnt/etc/sv/{{ item.item }}"
|
||||
dest: "/mnt/var/service/{{ item.item }}"
|
||||
state: link
|
||||
loop: "{{ configuration_runit_service_stats.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: item.stat.exists
|
||||
|
||||
@@ -15,16 +15,15 @@
|
||||
validate: /usr/sbin/visudo --check --file=%s
|
||||
|
||||
- name: Deploy per-user sudoers rules
|
||||
# Jinja truthiness: bool true / a rule string => deploy; false / '' / unset => skip.
|
||||
when: item.value.sudo | default(false)
|
||||
when: item.sudo | default(false)
|
||||
vars:
|
||||
configuration_sudoers_rule: >-
|
||||
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
|
||||
{{ item.sudo if item.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
|
||||
ansible.builtin.copy:
|
||||
content: "{{ item.key }} {{ configuration_sudoers_rule }}\n"
|
||||
dest: "/mnt/etc/sudoers.d/{{ item.key }}"
|
||||
content: "{{ item.name }} {{ configuration_sudoers_rule }}\n"
|
||||
dest: "/mnt/etc/sudoers.d/{{ item.name }}"
|
||||
mode: "0440"
|
||||
validate: /usr/sbin/visudo --check --file=%s
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
loop: "{{ system_cfg.users }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
label: "{{ item.name }}"
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
---
|
||||
- name: Set root password
|
||||
when: (system_cfg.root.password | default('') | string | length) > 0
|
||||
ansible.builtin.shell: >-
|
||||
set -o pipefail &&
|
||||
echo 'root:{{ system_cfg.root.password if (system_cfg.root.password | string)[:1] == "$" else system_cfg.root.password | password_hash("sha512") }}'
|
||||
| {{ chroot_command }} /usr/sbin/chpasswd -e
|
||||
echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} /usr/sbin/chpasswd -e
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: configuration_root_result
|
||||
changed_when: configuration_root_result.rc == 0
|
||||
no_log: true
|
||||
|
||||
- name: Lock root account when no password is set
|
||||
when: (system_cfg.root.password | default('') | string | length) == 0
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/bin/passwd -l root
|
||||
register: configuration_root_lock_result
|
||||
changed_when: configuration_root_lock_result.rc == 0
|
||||
|
||||
- name: Set root shell
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/sbin/usermod --shell {{ system_cfg.root.shell }} root
|
||||
@@ -27,47 +18,44 @@
|
||||
- name: Create user accounts
|
||||
vars:
|
||||
configuration_user_group: "{{ _configuration_platform.user_group }}"
|
||||
# plaintext is hashed; a pre-computed crypt hash ($6$/$y$/...) passes through.
|
||||
configuration_user_pw: >-
|
||||
{{ item.value.password if (item.value.password | string)[:1] == '$'
|
||||
else item.value.password | password_hash('sha512') }}
|
||||
# UID starts at 1000; safe for fresh installs only
|
||||
configuration_useradd_cmd: >-
|
||||
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
|
||||
--uid {{ 1000 + _idx }}
|
||||
--groups {{ configuration_user_group }} {{ item.key }}
|
||||
{{ ('--password ' ~ configuration_user_pw) if (item.value.password | default('') | string | length > 0) else '' }}
|
||||
--shell {{ item.value.shell | default('/bin/bash') }}
|
||||
--uid {{ 1000 + ansible_loop.index0 }}
|
||||
--groups {{ configuration_user_group }} {{ item.name }}
|
||||
--password {{ item.password | password_hash('sha512') }} --shell {{ item.shell | default('/bin/bash') }}
|
||||
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
loop: "{{ system_cfg.users }}"
|
||||
loop_control:
|
||||
index_var: _idx
|
||||
label: "{{ item.key }}"
|
||||
extended: true
|
||||
label: "{{ item.name }}"
|
||||
register: configuration_user_result
|
||||
changed_when: configuration_user_result.rc == 0
|
||||
no_log: true
|
||||
|
||||
- name: Ensure .ssh directory exists
|
||||
when: ('keys' in item.value) and (item.value['keys'] | length) > 0
|
||||
when: item['keys'] | default([]) | length > 0
|
||||
ansible.builtin.file:
|
||||
path: "/mnt/home/{{ item.key }}/.ssh"
|
||||
path: "/mnt/home/{{ item.name }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ 1000 + _idx }}"
|
||||
group: "{{ 1000 + _idx }}"
|
||||
owner: "{{ 1000 + ansible_loop.index0 }}"
|
||||
group: "{{ 1000 + ansible_loop.index0 }}"
|
||||
mode: "0700"
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
loop: "{{ system_cfg.users }}"
|
||||
loop_control:
|
||||
index_var: _idx
|
||||
label: "{{ item.key }}"
|
||||
extended: true
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Deploy SSH authorized_keys
|
||||
when: ('keys' in item.value) and (item.value['keys'] | length) > 0
|
||||
ansible.builtin.copy:
|
||||
content: "{{ item.value['keys'] | join('\n') }}\n"
|
||||
dest: "/mnt/home/{{ item.key }}/.ssh/authorized_keys"
|
||||
owner: "{{ 1000 + _idx }}"
|
||||
group: "{{ 1000 + _idx }}"
|
||||
- name: Add SSH public keys to authorized_keys
|
||||
vars:
|
||||
configuration_uid: "{{ 1000 + (system_cfg.users | map(attribute='name') | list).index(item.0.name) }}"
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/mnt/home/{{ item.0.name }}/.ssh/authorized_keys"
|
||||
line: "{{ item.1 }}"
|
||||
owner: "{{ configuration_uid }}"
|
||||
group: "{{ configuration_uid }}"
|
||||
mode: "0600"
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
create: true
|
||||
loop: "{{ system_cfg.users | subelements('keys', skip_missing=True) }}"
|
||||
loop_control:
|
||||
index_var: _idx
|
||||
label: "{{ item.key }}"
|
||||
label: "{{ item.0.name }}: {{ item.1[:40] }}..."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = _debian_release_map[os_version | string] | default('trixie') %}
|
||||
{% set mirror = system_cfg.content.url | default('http://deb.debian.org/debian', true) %}
|
||||
{% set components = 'main contrib non-free non-free-firmware' %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[{{ os }}{{ os_version_major }}-baseos]
|
||||
name={{ os }} {{ os_version_major }} BaseOS
|
||||
baseurl={{ system_cfg.content.url }}/BaseOS
|
||||
enabled=1
|
||||
gpgcheck={{ 1 if system_cfg.content.gpgcheck | bool else 0 }}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
|
||||
[{{ os }}{{ os_version_major }}-appstream]
|
||||
name={{ os }} {{ os_version_major }} AppStream
|
||||
baseurl={{ system_cfg.content.url }}/AppStream
|
||||
enabled=1
|
||||
gpgcheck={{ 1 if system_cfg.content.gpgcheck | bool else 0 }}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
@@ -1,4 +0,0 @@
|
||||
[daemon]
|
||||
WaylandEnable=true
|
||||
AutomaticLoginEnable=true
|
||||
AutomaticLogin={{ _desktop_autologin_user }}
|
||||
@@ -1,12 +0,0 @@
|
||||
[terminal]
|
||||
vt = 1
|
||||
|
||||
[default_session]
|
||||
command = "tuigreet --time --remember --cmd {{ _greetd_session }}"
|
||||
user = "greeter"
|
||||
{% if _desktop_autologin_user | length > 0 %}
|
||||
|
||||
[initial_session]
|
||||
command = "{{ _greetd_session }}"
|
||||
user = "{{ _desktop_autologin_user }}"
|
||||
{% endif %}
|
||||
@@ -3,18 +3,12 @@ id=LAN-{{ idx }}
|
||||
uuid={{ configuration_net_uuid }}
|
||||
type=ethernet
|
||||
autoconnect-priority=10
|
||||
{% set iface = configuration_iface %}
|
||||
{% if iface.name | default('') | string | length %}
|
||||
interface-name={{ iface.name }}
|
||||
|
||||
{% else %}
|
||||
{# Bind the first available ethernet by name glob, never a MAC: a clone with a new adapter/MAC stays networked (#12). #}
|
||||
|
||||
[match]
|
||||
interface-name=en*;eth*;
|
||||
|
||||
{% if configuration_iface_name | length > 0 %}
|
||||
interface-name={{ configuration_iface_name }}
|
||||
{% endif %}
|
||||
|
||||
[ipv4]
|
||||
{% set iface = configuration_iface %}
|
||||
{% set dns_list = configuration_dns_list %}
|
||||
{% set search_list = configuration_dns_search %}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
{% set ifname = iface.name if (iface.name | default('') | string | length) else (configuration_eni_detected[loop.index0] | default('eth' ~ loop.index0)) %}
|
||||
auto {{ ifname }}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
iface {{ ifname }} inet static
|
||||
address {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
gateway {{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_list %}
|
||||
dns-nameservers {{ configuration_dns_list | join(' ') }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_search %}
|
||||
dns-search {{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
iface {{ ifname }} inet dhcp
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
@@ -1,29 +0,0 @@
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
lan{{ loop.index0 }}:
|
||||
{# Unnamed binds the first ethernet by name glob (e* = en*/eth*, netplan match.name takes one glob), never a MAC (#12). #}
|
||||
match:
|
||||
name: "{{ iface.name if (iface.name | default('') | string | length) else 'e*' }}"
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
addresses:
|
||||
- {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
routes:
|
||||
- to: default
|
||||
via: {{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
dhcp4: true
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and (configuration_dns_list or configuration_dns_search) %}
|
||||
nameservers:
|
||||
{% if configuration_dns_list %}
|
||||
addresses: [{{ configuration_dns_list | join(', ') }}]
|
||||
{% endif %}
|
||||
{% if configuration_dns_search %}
|
||||
search: [{{ configuration_dns_search | join(', ') }}]
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -1,27 +0,0 @@
|
||||
[Match]
|
||||
{% set iface = configuration_iface %}
|
||||
{% if iface.name | default('') | string | length %}
|
||||
Name={{ iface.name }}
|
||||
{% else %}
|
||||
{# First available ethernet by name glob + device type, never a MAC (#12). #}
|
||||
Name=en* eth*
|
||||
Type=ether
|
||||
{% endif %}
|
||||
|
||||
[Network]
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
Address={{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
Gateway={{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
DHCP=yes
|
||||
{% endif %}
|
||||
{% if idx | int == 0 and configuration_dns_list %}
|
||||
{% for dns in configuration_dns_list %}
|
||||
DNS={{ dns }}
|
||||
{% endfor %}
|
||||
{% if configuration_dns_search %}
|
||||
Domains={{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -1,6 +0,0 @@
|
||||
{% set _session = _desktop_session if (_desktop_session | length > 0) else _sddm_session %}
|
||||
[Autologin]
|
||||
User={{ _desktop_autologin_user }}
|
||||
{% if _session | length > 0 %}
|
||||
Session={{ _session }}
|
||||
{% endif %}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = _ubuntu_release_map[os] | default('resolute') %}
|
||||
{% set mirror = system_cfg.content.url %}
|
||||
{% set release = _ubuntu_release_map[os] | default('noble') %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main restricted universe multiverse' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
# Keyed by os_family; tasks read configuration_platform_config[os_family] as _configuration_platform.
|
||||
# Platform-specific configuration values keyed by os_family.
|
||||
# Consumed as _configuration_platform in tasks via:
|
||||
# configuration_platform_config[os_family]
|
||||
configuration_platform_config:
|
||||
RedHat:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
time_sync_service: chronyd
|
||||
efi_loader: shimx64.efi
|
||||
grub_install: false
|
||||
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
|
||||
@@ -16,7 +17,6 @@ configuration_platform_config:
|
||||
user_group: sudo
|
||||
sudo_group: "%sudo"
|
||||
ssh_service: ssh
|
||||
time_sync_service: chrony
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: >-
|
||||
@@ -29,27 +29,39 @@ configuration_platform_config:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
time_sync_service: systemd-timesyncd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: "/usr/sbin/mkinitcpio -P"
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: true
|
||||
init_system: systemd
|
||||
|
||||
configuration_desktop_dm_map:
|
||||
gnome: gdm
|
||||
kde: sddm
|
||||
sway: greetd
|
||||
hyprland: greetd
|
||||
|
||||
# greetd session commands for sway/hyprland (gnome/kde use a DM instead).
|
||||
configuration_desktop_session_cmd_map:
|
||||
sway: sway
|
||||
hyprland: Hyprland
|
||||
|
||||
# pipewire/pipewire-pulse are socket-activated; wireplumber ships no socket.
|
||||
configuration_desktop_audio_units:
|
||||
- pipewire.socket
|
||||
- pipewire-pulse.socket
|
||||
- wireplumber.service
|
||||
Suse:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: true
|
||||
init_system: systemd
|
||||
Alpine:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: ""
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: false
|
||||
init_system: openrc
|
||||
Void:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: ""
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: false
|
||||
init_system: runit
|
||||
|
||||
@@ -1,60 +1,10 @@
|
||||
---
|
||||
# Connection and timing
|
||||
environment_wait_timeout: 180
|
||||
environment_wait_delay: 5
|
||||
|
||||
# Pacman installer settings
|
||||
environment_parallel_downloads: 20
|
||||
environment_pacman_lock_timeout: 120
|
||||
environment_pacman_retries: 4
|
||||
environment_pacman_retry_delay: 15
|
||||
|
||||
# Installer-tool libraries whose soname may have bumped past the ISO. Each one's
|
||||
# installed reverse-deps are co-upgraded so the install stays a consistent
|
||||
# transaction. Extend if a future transition breaks the install.
|
||||
environment_partial_upgrade_libs:
|
||||
- nettle
|
||||
- leancrypto
|
||||
|
||||
# PCI vendor ID -> vendor code. Only vendors that drive distinct
|
||||
# firmware/driver packages are mapped.
|
||||
environment_pci_vendor_map:
|
||||
"8086": intel
|
||||
"1002": amd
|
||||
"1022": amd
|
||||
"10de": nvidia
|
||||
"14e4": broadcom
|
||||
"10ec": realtek
|
||||
"168c": atheros
|
||||
"0cf3": atheros
|
||||
"168d": atheros
|
||||
"14c3": mediatek
|
||||
"11ab": marvell
|
||||
"1b4b": marvell
|
||||
"17cb": qcom
|
||||
"105b": qcom
|
||||
"1cf3": cirrus
|
||||
"13d7": cirrus
|
||||
|
||||
# USB vendor IDs of fingerprint readers supported by libfprint / fprintd,
|
||||
# matched against `lsusb` output.
|
||||
environment_fingerprint_vendor_ids:
|
||||
- "06cb" # Synaptics (modern ThinkPad/Dell)
|
||||
- "138a" # Validity Sensors (older ThinkPad)
|
||||
- "1c7a" # LighTuning / Egis
|
||||
- "27c6" # Goodix
|
||||
- "04f3" # Elan
|
||||
- "0a5c" # Broadcom
|
||||
- "08ff" # AuthenTec (legacy)
|
||||
- "147e" # Upek (legacy)
|
||||
- "1491" # Futronic
|
||||
|
||||
# USB vendor IDs of common Bluetooth controllers. A fallback: detection also
|
||||
# matches the literal "Bluetooth" string in `lsusb` for adapters that omit it.
|
||||
environment_bluetooth_vendor_ids:
|
||||
- "8087" # Intel (AX2xx combo cards)
|
||||
- "0a12" # Cambridge Silicon Radio (CSR)
|
||||
- "0bda" # Realtek
|
||||
- "0cf3" # Qualcomm Atheros
|
||||
- "13d3" # IMC / AzureWave
|
||||
- "0489" # Foxconn / Lite-On
|
||||
- "04ca" # Lite-On
|
||||
- "0b05" # ASUS
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
- name: Restart sshd
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
@@ -87,10 +87,9 @@
|
||||
- name: Switch to SSH connection
|
||||
ansible.builtin.set_fact:
|
||||
ansible_connection: ssh
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_port: 22
|
||||
ansible_user: root
|
||||
ansible_password: ""
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
|
||||
- name: Reset connection for SSH switchover
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
---
|
||||
# A user-supplied override profile skips detection (golden-image flow: bake an
|
||||
# image with a fixed profile).
|
||||
- name: Resolve hardware detection requirement
|
||||
ansible.builtin.set_fact:
|
||||
_hardware_detection_needed: >-
|
||||
{{
|
||||
(system_cfg.features.firmware.enabled | bool)
|
||||
or (system_cfg.features.gpu.enabled | bool)
|
||||
or (system_cfg.features.peripherals.enabled | bool)
|
||||
}}
|
||||
_hardware_profile_override: "{{ system_cfg.features.hardware.profile | default({}) }}"
|
||||
|
||||
- name: Use supplied hardware profile (override)
|
||||
when:
|
||||
- _hardware_detection_needed | bool
|
||||
- _hardware_profile_override | length > 0
|
||||
ansible.builtin.set_fact:
|
||||
hardware_profile_active:
|
||||
cpu: "{{ _hardware_profile_override.cpu | default('') | string | lower }}"
|
||||
gpus: "{{ _hardware_profile_override.gpus | default([]) | map('lower') | list }}"
|
||||
nvidia_supports_open: "{{ _hardware_profile_override.nvidia_supports_open | default(true) | bool }}"
|
||||
wireless: "{{ _hardware_profile_override.wireless | default([]) | map('lower') | list }}"
|
||||
audio: "{{ _hardware_profile_override.audio | default([]) | map('lower') | list }}"
|
||||
fingerprint: "{{ _hardware_profile_override.fingerprint | default(false) | bool }}"
|
||||
bluetooth: "{{ _hardware_profile_override.bluetooth | default(false) | bool }}"
|
||||
camera:
|
||||
uvc: "{{ _hardware_profile_override.camera.uvc | default(false) | bool }}"
|
||||
ipu6: "{{ _hardware_profile_override.camera.ipu6 | default(false) | bool }}"
|
||||
|
||||
- name: Detect hardware from live host
|
||||
when:
|
||||
- _hardware_detection_needed | bool
|
||||
- _hardware_profile_override | length == 0
|
||||
block:
|
||||
- name: Read CPU vendor
|
||||
ansible.builtin.command: lscpu
|
||||
register: _hardware_lscpu
|
||||
changed_when: false
|
||||
|
||||
- name: Read PCI device list
|
||||
ansible.builtin.command: lspci -nn
|
||||
register: _hardware_lspci
|
||||
changed_when: false
|
||||
|
||||
- name: Read USB device list
|
||||
ansible.builtin.command: lsusb
|
||||
register: _hardware_lsusb
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Resolve detected hardware profile
|
||||
ansible.builtin.include_tasks: _resolve_hardware_profile.yml
|
||||
|
||||
- name: Initialize empty hardware profile when detection skipped
|
||||
when: not (_hardware_detection_needed | bool)
|
||||
ansible.builtin.set_fact:
|
||||
hardware_profile_active:
|
||||
cpu: ""
|
||||
gpus: []
|
||||
nvidia_supports_open: true
|
||||
wireless: []
|
||||
audio: []
|
||||
fingerprint: false
|
||||
bluetooth: false
|
||||
camera: { uvc: false, ipu6: false }
|
||||
|
||||
- name: Merge declarative hardware group over detection
|
||||
when: _hardware_detection_needed | bool
|
||||
ansible.builtin.include_tasks: _merge_hardware_profile.yml
|
||||
|
||||
- name: Report active hardware profile
|
||||
when: _hardware_detection_needed | bool
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Hardware profile {{ 'override' if _hardware_profile_override | length > 0 else 'detected' }}:
|
||||
cpu={{ hardware_profile_active.cpu | default('-') }},
|
||||
gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }}
|
||||
{{ '(open-supported)' if hardware_profile_active.nvidia_supports_open | bool else '(legacy)' }},
|
||||
wireless={{ hardware_profile_active.wireless | default([]) | join(',') | default('-', true) }},
|
||||
audio={{ hardware_profile_active.audio | default([]) | join(',') | default('-', true) }},
|
||||
fingerprint={{ hardware_profile_active.fingerprint | default(false) }},
|
||||
bluetooth={{ hardware_profile_active.bluetooth | default(false) }},
|
||||
camera={{ 'uvc' if hardware_profile_active.camera.uvc | default(false) else '' }}{{ '+ipu6' if hardware_profile_active.camera.ipu6 | default(false) else '' }}
|
||||
@@ -68,20 +68,6 @@
|
||||
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
|
||||
quiet: true
|
||||
|
||||
- name: Harden sshd for Ansible automation
|
||||
ansible.builtin.blockinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
marker: "# {mark} BOOTSTRAP ANSIBLE SETTINGS"
|
||||
block: |
|
||||
PerSourcePenalties no
|
||||
MaxStartups 50:30:100
|
||||
ClientAliveInterval 30
|
||||
ClientAliveCountMax 10
|
||||
notify: Restart sshd
|
||||
|
||||
- name: Apply pending sshd restart before continuing
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Abort if the host is not booted from the Arch install media
|
||||
when:
|
||||
- not (custom_iso | bool)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
# Supplements whatever profile is active (detected or full-override) rather than
|
||||
# replacing it: vendor lists union, booleans OR, cpu overrides when set.
|
||||
- name: Merge declarative hardware group over detection
|
||||
vars:
|
||||
_hw: "{{ system_cfg.features.hardware }}"
|
||||
_det: "{{ hardware_profile_active }}"
|
||||
ansible.builtin.set_fact:
|
||||
hardware_profile_active:
|
||||
cpu: "{{ (_hw.cpu | default('') | string | lower) if (_hw.cpu | default('') | length > 0) else _det.cpu }}"
|
||||
gpus: "{{ ((_det.gpus | default([])) + (_hw.gpus | default([]) | map('lower') | list)) | unique | list }}"
|
||||
nvidia_supports_open: "{{ _det.nvidia_supports_open | default(true) | bool }}"
|
||||
wireless: "{{ ((_det.wireless | default([])) + (_hw.wireless | default([]) | map('lower') | list)) | unique | list }}"
|
||||
audio: "{{ ((_det.audio | default([])) + (_hw.audio | default([]) | map('lower') | list)) | unique | list }}"
|
||||
fingerprint: "{{ (_det.fingerprint | default(false) | bool) or (_hw.fingerprint | default(false) | bool) }}"
|
||||
bluetooth: "{{ (_det.bluetooth | default(false) | bool) or (_hw.bluetooth | default(false) | bool) }}"
|
||||
camera:
|
||||
uvc: "{{ (_det.camera.uvc | default(false) | bool) or (_hw.camera.uvc | default(false) | bool) }}"
|
||||
ipu6: "{{ (_det.camera.ipu6 | default(false) | bool) or (_hw.camera.ipu6 | default(false) | bool) }}"
|
||||
_hardware_profile_packages: "{{ _hw.packages | default({}) }}"
|
||||
_hardware_profile_disable: "{{ _hw.disable | default([]) | list }}"
|
||||
_hardware_profile_kernel_params: "{{ _hw.kernel_params | default([]) | list }}"
|
||||
@@ -14,52 +14,23 @@
|
||||
timeout: "{{ environment_pacman_lock_timeout }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Resolve installer tools for the target OS
|
||||
when: not (custom_iso | bool)
|
||||
ansible.builtin.set_fact:
|
||||
environment_installer_tools: >-
|
||||
{{
|
||||
['glibc']
|
||||
+ (['lua', 'dnf'] if os in ['almalinux', 'fedora', 'rhel', 'rocky'] else [])
|
||||
+ (['debootstrap'] if os in ['debian', 'ubuntu', 'ubuntu-lts'] else [])
|
||||
+ (['debian-archive-keyring'] if os == 'debian' else [])
|
||||
+ (['ubuntu-keyring'] if os in ['ubuntu', 'ubuntu-lts'] else [])
|
||||
}}
|
||||
|
||||
- name: Query reverse-dependencies of transition-sensitive libraries
|
||||
- name: Setup Pacman
|
||||
when:
|
||||
- not (custom_iso | bool)
|
||||
- environment_partial_upgrade_libs | length > 0
|
||||
ansible.builtin.command: "pacman -Qi {{ item }}"
|
||||
loop: "{{ environment_partial_upgrade_libs }}"
|
||||
register: environment_revdep_query
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
# Co-upgrade each transition library with its installed reverse-deps so a soname
|
||||
# bump moves the whole closure in one transaction, not a partial upgrade.
|
||||
- name: Setup Pacman
|
||||
when: not (custom_iso | bool)
|
||||
vars:
|
||||
environment_pacman_closure: >-
|
||||
{{
|
||||
(
|
||||
environment_installer_tools
|
||||
+ (environment_revdep_query.results | default([])
|
||||
| selectattr('rc', 'equalto', 0) | map(attribute='item') | list)
|
||||
+ (environment_revdep_query.results | default([])
|
||||
| selectattr('rc', 'equalto', 0) | map(attribute='stdout')
|
||||
| map('regex_search', 'Required By\s*:\s*(.+)', '\1')
|
||||
| map('first') | map('split') | flatten)
|
||||
)
|
||||
| reject('equalto', 'None') | unique
|
||||
}}
|
||||
- item.os is not defined or os in item.os
|
||||
community.general.pacman:
|
||||
update_cache: true
|
||||
name: "{{ environment_pacman_closure }}"
|
||||
force: true
|
||||
name: "{{ item.name }}"
|
||||
state: latest
|
||||
register: environment_tool_install
|
||||
until: environment_tool_install is succeeded
|
||||
loop:
|
||||
- { name: glibc }
|
||||
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
|
||||
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
|
||||
- { name: debian-archive-keyring, os: [debian] }
|
||||
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
retries: "{{ environment_pacman_retries }}"
|
||||
delay: "{{ environment_pacman_retry_delay }}"
|
||||
|
||||
@@ -73,25 +44,28 @@
|
||||
mode: "0755"
|
||||
|
||||
- name: Detect RHEL ISO device
|
||||
ansible.builtin.command: lsblk -rbno NAME,TYPE,SIZE
|
||||
ansible.builtin.command: lsblk -rno NAME,TYPE
|
||||
register: environment_lsblk_result
|
||||
changed_when: false
|
||||
|
||||
- name: Select RHEL ISO device
|
||||
vars:
|
||||
_roms: >-
|
||||
{%- set out = [] -%}
|
||||
{%- for line in environment_lsblk_result.stdout_lines -%}
|
||||
{%- set p = line.split() -%}
|
||||
{%- if (p | length) >= 3 and p[1] == 'rom' -%}
|
||||
{%- set _ = out.append({'name': p[0], 'size': p[2] | int}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
_rom_devices: >-
|
||||
{{
|
||||
environment_lsblk_result.stdout_lines
|
||||
| map('split', ' ')
|
||||
| selectattr('1', 'equalto', 'rom')
|
||||
| map('first')
|
||||
| map('regex_replace', '^', '/dev/')
|
||||
| list
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
environment_rhel_iso_device: >-
|
||||
{{ ('/dev/' ~ (_roms | sort(attribute='size') | last).name)
|
||||
if (_roms | length) > 0 else '/dev/sr1' }}
|
||||
{{
|
||||
_rom_devices[-1]
|
||||
if _rom_devices | length > 1
|
||||
else (_rom_devices[0] | default('/dev/sr1'))
|
||||
}}
|
||||
|
||||
- name: Mount RHEL ISO
|
||||
ansible.posix.mount:
|
||||
@@ -101,8 +75,10 @@
|
||||
opts: "ro,loop"
|
||||
state: mounted
|
||||
|
||||
# RPM Sequoia signature policy is relaxed because the Arch ISO host does not
|
||||
# trust target-distro GPG keys; the target's own rpm re-verifies after reboot.
|
||||
# Security note: RPM Sequoia signature policy is relaxed to allow
|
||||
# bootstrapping RHEL-family distros from the Arch ISO, where the
|
||||
# host rpm/dnf does not trust target distro GPG keys. Package
|
||||
# integrity is verified by the target system's own rpm after reboot.
|
||||
- name: Create RPM macros directory
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.file:
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
# Split out of _detect_hardware.yml so fixtures can seed the lscpu/lspci/lsusb
|
||||
# registers and assert the result with no real hardware. Keep regex exprs
|
||||
# double-quoted single-line: ansible-core 2.21 set_fact mangles backslash escapes
|
||||
# inside folded (>-) scalars.
|
||||
- name: Resolve detected hardware profile
|
||||
vars:
|
||||
_vendor_keys: "{{ environment_pci_vendor_map.keys() | list }}"
|
||||
_cpu_vendor_raw: "{{ _hardware_lscpu.stdout | regex_findall('(?im)^Vendor ID:\\s*(\\S+)') | first | default('') }}"
|
||||
_cpu_vendor: >-
|
||||
{{
|
||||
'intel' if _cpu_vendor_raw == 'GenuineIntel'
|
||||
else ('amd' if _cpu_vendor_raw == 'AuthenticAMD' else '')
|
||||
}}
|
||||
# PCI classes: 0300 = VGA, 0302 = 3D, 0280 = wireless network controller.
|
||||
_gpu_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[(0300|0302)\\]:') | list }}"
|
||||
_gpu_pairs: "{{ (_gpu_lines | join('\n')) | regex_findall('\\[([0-9a-f]{4}):([0-9a-f]{4})\\]') | list }}"
|
||||
_gpu_vendor_ids: "{{ _gpu_pairs | map('first') | select('in', _vendor_keys) | list }}"
|
||||
_gpu_vendors: "{{ _gpu_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
|
||||
_nvidia_device_ids: "{{ _gpu_pairs | selectattr('0', 'equalto', '10de') | map(attribute=1) | list }}"
|
||||
_nvidia_min_id: >-
|
||||
{{
|
||||
(_nvidia_device_ids | map('int', base=16) | list | min)
|
||||
if _nvidia_device_ids | length > 0 else 0
|
||||
}}
|
||||
# 0x1e00 = 7680 = first Turing device id; Turing+ supports nvidia-open.
|
||||
_nvidia_supports_open: "{{ _nvidia_device_ids | length > 0 and (_nvidia_min_id | int) >= 7680 }}"
|
||||
_wifi_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0280\\]:') | list }}"
|
||||
_wifi_vendor_ids: "{{ (_wifi_lines | join('\n')) | regex_findall('\\[([0-9a-f]{4}):[0-9a-f]{4}\\]') | select('in', _vendor_keys) | list }}"
|
||||
_wifi_vendors: "{{ _wifi_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
|
||||
# PCI class 0403 = audio device (HD-audio controller). Vendor drives SOF/firmware.
|
||||
_audio_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0403\\]:') | list }}"
|
||||
_audio_vendor_ids: "{{ (_audio_lines | join('\n')) | regex_findall('\\[([0-9a-f]{4}):[0-9a-f]{4}\\]') | select('in', _vendor_keys) | list }}"
|
||||
_audio_vendors: "{{ _audio_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
|
||||
_fingerprint_present: "{{ (_hardware_lsusb.stdout | default('')) | regex_search('(?i)ID (' ~ (environment_fingerprint_vendor_ids | join('|')) ~ '):') is not none }}"
|
||||
_camera_uvc_present: "{{ (_hardware_lsusb.stdout | default('')) is search('(?i)camera|webcam') }}"
|
||||
# Intel IPU6 MIPI camera: PCI class 0480 (multimedia) under Intel 8086, or an ISP description. Out-of-tree userspace.
|
||||
_camera_ipu6_desc: "{{ (_hardware_lspci.stdout | default('')) is search('(?i)image signal processor|IPU6') }}"
|
||||
_camera_ipu6_pci: "{{ (_hardware_lspci.stdout_lines | select('search', '\\[0480\\]:') | select('search', '\\[8086:') | list) | length > 0 }}"
|
||||
# No backslash escapes here, so a folded scalar is safe (unlike the \[..\] regexes above).
|
||||
_bluetooth_present: >-
|
||||
{{
|
||||
((_hardware_lsusb.stdout | default('')) | regex_search('(?i)ID (' ~ (environment_bluetooth_vendor_ids | join('|')) ~ '):') is not none)
|
||||
or ((_hardware_lsusb.stdout | default('')) is search('(?i)bluetooth'))
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
hardware_profile_active:
|
||||
cpu: "{{ _cpu_vendor }}"
|
||||
gpus: "{{ _gpu_vendors }}"
|
||||
nvidia_supports_open: "{{ _nvidia_supports_open | bool }}"
|
||||
wireless: "{{ _wifi_vendors }}"
|
||||
audio: "{{ _audio_vendors }}"
|
||||
fingerprint: "{{ _fingerprint_present | bool }}"
|
||||
bluetooth: "{{ _bluetooth_present | bool }}"
|
||||
camera:
|
||||
uvc: "{{ _camera_uvc_present | bool }}"
|
||||
ipu6: "{{ (_camera_ipu6_desc | bool) or (_camera_ipu6_pci | bool) }}"
|
||||
@@ -11,8 +11,5 @@
|
||||
- name: Prepare installer environment
|
||||
ansible.builtin.include_tasks: _prepare_installer.yml
|
||||
|
||||
- name: Detect hardware for firmware/GPU package selection
|
||||
ansible.builtin.include_tasks: _detect_hardware.yml
|
||||
|
||||
- name: Run third-party preparation tasks
|
||||
ansible.builtin.include_tasks: _thirdparty.yml
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# gpgcheck off: bootstrap-time only; the Arch live env has no AlmaLinux key.
|
||||
[appstream]
|
||||
name=AlmaLinux $releasever - AppStream
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/appstream
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/AppStream/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
gpgcheck=1
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
@@ -15,7 +14,7 @@ name=AlmaLinux $releasever - BaseOS
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/BaseOS/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
gpgcheck=1
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
@@ -26,7 +25,7 @@ name=AlmaLinux $releasever - Extras
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/extras
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/extras/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
gpgcheck=1
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
@@ -37,7 +36,7 @@ name=AlmaLinux $releasever - HighAvailability
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/highavailability
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/HighAvailability/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
gpgcheck=1
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
{% set _baseurl = system_cfg.content.url if system_cfg.content.source == 'mirror' else 'file:///usr/local/install/redhat/dvd' %}
|
||||
[rhel{{ os_version_major }}-baseos]
|
||||
name=RHEL {{ os_version_major }} BaseOS
|
||||
baseurl={{ _baseurl }}/BaseOS
|
||||
baseurl=file:///usr/local/install/redhat/dvd/BaseOS
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
{% if system_cfg.content.source != 'mirror' %}
|
||||
gpgkey=file:///usr/local/install/redhat/dvd/RPM-GPG-KEY-redhat-release
|
||||
{% endif %}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
|
||||
[rhel{{ os_version_major }}-appstream]
|
||||
name=RHEL {{ os_version_major }} AppStream
|
||||
baseurl={{ _baseurl }}/AppStream
|
||||
baseurl=file:///usr/local/install/redhat/dvd/AppStream
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
{% if system_cfg.content.source != 'mirror' %}
|
||||
gpgkey=file:///usr/local/install/redhat/dvd/RPM-GPG-KEY-redhat-release
|
||||
{% endif %}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# gpgcheck off: bootstrap-time only; the Arch live env has no Rocky key.
|
||||
[baseos]
|
||||
name=Rocky Linux $releasever - BaseOS
|
||||
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=BaseOS-$releasever
|
||||
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/BaseOS/$basearch/os/
|
||||
gpgcheck=0
|
||||
gpgcheck=1
|
||||
enabled=1
|
||||
countme=1
|
||||
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
|
||||
@@ -14,7 +13,7 @@ enabled_metadata=1
|
||||
name=Rocky Linux $releasever - AppStream
|
||||
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=AppStream-$releasever
|
||||
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/AppStream/$basearch/os/
|
||||
gpgcheck=0
|
||||
gpgcheck=1
|
||||
enabled=1
|
||||
countme=1
|
||||
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user