From 02157bb6018cddc570988c5ce7f4e2ebfc49f1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Domingo=20Agust=C3=AD?= Date: Tue, 22 Sep 2026 21:35:08 +0200 Subject: [PATCH] feat(luks): cifrado de disco opcional con LUKS2 (keyfile/passphrase) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Añade soporte para cifrado LUKS2 de raíz y home. - Métodos: keyfile en /boot (arranque desatendido) o passphrase (interactivo). - Extiende cfg_get_nested() para leer configuración anidada. - Actualiza perfiles JSON: production cifrado por defecto. - Ajusta paquetes: añade okular, elimina audacity/shotwell. - Configura crypttab, mkinitcpio (hook encrypt) y GRUB cmdline. - Soporte API para encryption y password personalizados. - Tests de utilidades bash y Jest actualizados. - Documentación: docs/INSTALL.md §6.1 y docs/PACKAGES.md. --- configs/base.json | 12 ++- configs/developer.json | 16 +++- configs/production.json | 11 ++- docs/INSTALL.md | 57 +++++++++++- docs/PACKAGES.md | 133 ++++++++++++++++++++++++++++ portal/routes/install.js | 18 +++- portal/tests/routes/install.test.js | 26 ++++++ scripts/10-partition.sh | 52 ++++++++++- scripts/20-archinstall.sh | 24 +++++ scripts/30-postinstall.sh | 65 +++++++++++++- scripts/lib/utils.sh | 27 ++++++ tests/bash/utils.bats | 22 ++++- 12 files changed, 450 insertions(+), 13 deletions(-) create mode 100644 docs/PACKAGES.md diff --git a/configs/base.json b/configs/base.json index 8ae335e..0db7e67 100644 --- a/configs/base.json +++ b/configs/base.json @@ -11,7 +11,8 @@ "fastfetch", "git", "curl", - "wget" + "wget", + "okular" ], "services": [ "NetworkManager", @@ -19,5 +20,12 @@ ], "timezone": "Europe/Madrid", "locale": "es_ES.UTF-8", - "keyboard": "es" + "keyboard": "es", + "encryption": { + "enabled": false, + "method": "keyfile", + "passphrase": "", + "cipher": "aes-xts-plain64", + "key_size": 512 + } } diff --git a/configs/developer.json b/configs/developer.json index 894a345..d7fbcf3 100644 --- a/configs/developer.json +++ b/configs/developer.json @@ -17,13 +17,18 @@ "go", "rust", "code", + "jetbrains-toolbox", + "postman-bin", + "insomnia", "github-cli", + "gitlab-runner", "kubectl", "helm", "minikube", "terraform", "ansible", - "git" + "git", + "okular" ], "services": [ "NetworkManager", @@ -32,5 +37,12 @@ ], "timezone": "Europe/Madrid", "locale": "es_ES.UTF-8", - "keyboard": "es" + "keyboard": "es", + "encryption": { + "enabled": false, + "method": "keyfile", + "passphrase": "", + "cipher": "aes-xts-plain64", + "key_size": 512 + } } diff --git a/configs/production.json b/configs/production.json index e92f8ac..0c65242 100644 --- a/configs/production.json +++ b/configs/production.json @@ -24,7 +24,8 @@ "firefox", "libreoffice-fresh", "vlc", - "gimp" + "gimp", + "okular" ], "services": [ "NetworkManager", @@ -46,5 +47,13 @@ "schedule": "daily", "target": "/home/neubat/backups" } + }, + "encryption": { + "enabled": true, + "method": "keyfile", + "passphrase": "neubat", + "cipher": "aes-xts-plain64", + "key_size": 512, + "_warning": "El método 'keyfile' almacena la llave en /boot para arranque desatendido. Para mayor seguridad física cambia a 'passphrase' tras la instalación o usa TPM2." } } diff --git a/docs/INSTALL.md b/docs/INSTALL.md index b634895..c8f65f8 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -65,7 +65,7 @@ Como servicio systemd, usar como plantilla la unidad que genera `scripts/40-port | Método | Ruta | Descripción | |--------|------|-------------| -| POST | `/api/install` | Crea instalación; body: `profile`, `hostname?`, `username?`, `desktop?`, `packages?[]` | +| POST | `/api/install` | Crea instalación; body: `profile`, `hostname?`, `username?`, `password?`, `desktop?`, `packages?[]`, `encryption?` | | GET | `/api/config/:token` | Devuelve el JSON de configuración (consumido por el instalador) | | POST | `/api/complete` | El instalador notifica `status`, `hostname`, `error?` | | GET | `/api/installations` | Últimas 50 instalaciones | @@ -164,6 +164,61 @@ bash scripts/neubat-install.sh [perfil] Los nombres de partición se resuelven con `part_name()` (soporta `/dev/sda1` y `/dev/nvme0n1p1`). +## 6.1 Cifrado de disco LUKS (Fase 6) + +NEUBAT puede cifrar las particiones de **raíz** y **home** con LUKS2. La partición EFI (`/boot/efi`) permanece descifrada porque el firmware UEFI debe poder leer el cargador de arranque. + +### Modos de arranque + +| Método | Campo `encryption.method` | Comportamiento | Seguridad | +|--------|---------------------------|----------------|-----------| +| **Keyfile en `/boot`** | `keyfile` | Arranque completamente desatendido | Protege datos en reposo si el disco está apagado; no protege si roban el disco con la partición EFI | +| **Passphrase manual** | `passphrase` | El initramfs pide la contraseña en cada arranque | Mayor seguridad física; rompe el despliegue zero-touch | + +### Configuración en el perfil + +```json +{ + "encryption": { + "enabled": true, + "method": "keyfile", + "passphrase": "cambiar-post-instalacion", + "cipher": "aes-xts-plain64", + "key_size": 512 + } +} +``` + +- `enabled`: activa/desactiva LUKS. +- `method`: `keyfile` (desatendido) o `passphrase` (interactivo). +- `passphrase`: se usa para formatear el contenedor cuando no hay keyfile; también puede usarse para añadir frases adicionales tras la instalación. +- `cipher` / `key_size`: parámetros de `cryptsetup luksFormat` (defecto `aes-xts-plain64` / 512). + +### Desde la API + +```bash +curl -X POST http://:3000/api/install \ + -H 'Content-Type: application/json' \ + -d '{ + "profile": "production", + "hostname": "mi-equipo", + "encryption": { "enabled": true, "method": "passphrase", "passphrase": "MiFraseSegura" } + }' +``` + +### Post-instalación recomendada + +Cuando uses `method: "keyfile"`, rota la llave tras el primer arranque: + +```bash +# Añade una passphrase y elimina el keyfile del slot 0 +sudo cryptsetup luksAddKey /dev/nvme0n1p2 +sudo cryptsetup luksRemoveKey /dev/nvme0n1p2 /boot/luks-keyfile +sudo rm /boot/luks-keyfile +``` + +Para TPM2 o FIDO2, consulta `systemd-cryptenroll` (fuera del alcance del MVP). + ## 7. Perfiles de configuración Los perfiles viven en `configs/` (`base`, `production`, `developer`). Claves: diff --git a/docs/PACKAGES.md b/docs/PACKAGES.md new file mode 100644 index 0000000..2293a05 --- /dev/null +++ b/docs/PACKAGES.md @@ -0,0 +1,133 @@ +# NEUBAT — Panel de paquetes por perfil + +Este documento lista los paquetes que vienen predefinidos en cada perfil de instalación. Los paquetes marcados con **(AUR)** no están en los repositorios oficiales de Arch Linux y se instalan con `yay` tras el primer arranque si fallan durante la instalación desatendida. + +## Leyenda + +| Símbolo | Significado | +|---------|-------------| +| 🖥️ | Entorno de escritorio / gestor de ventanas | +| 🔧 | Herramientas de desarrollo/sistema | +| 🌐 | Red / Internet | +| 🛡️ | Seguridad / cifrado | +| 🎨 | Multimedia / productividad | +| 📦 | Servicios / infraestructura | + +--- + +## Perfil `base` (mínimo) + +Escritorio: `none` · Disco: `/dev/sda` · Cifrado: desactivado + +| Paquete | Categoría | Descripción | +|---------|-----------|-------------| +| `htop` | 🔧 | Monitor de procesos interactivo | +| `btop` | 🔧 | Monitor de recursos con gráficos ANSI | +| `fastfetch` | 🔧 | Información del sistema (sustituto moderno de neofetch) | +| `git` | 🔧 | Control de versiones | +| `curl` | 🌐 | Cliente HTTP/HTTPS | +| `wget` | 🌐 | Descarga de archivos por HTTP/FTP | +| `okular` | 🎨 | Visor de documentos universal (PDF, ePub, DjVu, etc.) | + +Servicios habilitados: `NetworkManager`, `sshd` + +--- + +## Perfil `production` (escritorio KDE + apps ofimáticas) + +Escritorio: `kde` · Disco: `/dev/nvme0n1` · Cifrado: **activado con keyfile** + +| Paquete | Categoría | Descripción | +|---------|-----------|-------------| +| `plasma-meta` | 🖥️ | Escritorio KDE Plasma | +| `kde-applications-meta` | 🖥️ | Aplicaciones básicas de KDE | +| `sddm` | 🖥️ | Gestor de pantalla | +| `docker` | 📦 | Contenedores | +| `docker-compose` | 📦 | Orquestación de contenedores | +| `nodejs` / `npm` | 🔧 | Runtime y gestor de paquetes JavaScript | +| `python` / `python-pip` | 🔧 | Python 3 y pip | +| `nginx` | 📦 | Servidor web/proxy inverso | +| `postgresql` | 📦 | Base de datos relacional | +| `redis` | 📦 | Almacén clave-valor en memoria | +| `htop` / `btop` / `fastfetch` | 🔧 | Monitores del sistema | +| `git` / `curl` / `wget` | 🔧🌐 | Desarrollo y red | +| `firefox` | 🌐 | Navegador web | +| `libreoffice-fresh` | 🎨 | Suite ofimática | +| `vlc` | 🎨 | Reproductor multimedia | +| `gimp` | 🎨 | Edición de imágenes | +| `okular` | 🎨 | Visor de documentos | + +Servicios habilitados: `NetworkManager`, `sshd`, `docker`, `nginx`, `postgresql`, `redis` + +### Cambios recientes + +- **Añadido:** `okular` (visor de documentos). +- **Eliminados:** `audacity`, `shotwell`. Si necesitas gestión fotográfica avanzada, instala `digiKam` desde KDE. + +--- + +## Perfil `developer` (escritorio GNOME + toolchain) + +Escritorio: `gnome` · Disco: `/dev/sda` · Cifrado: desactivado + +| Paquete | Categoría | Descripción | +|---------|-----------|-------------| +| `gnome` / `gnome-extra` | 🖥️ | Escritorio GNOME y aplicaciones extra | +| `gdm` | 🖥️ | Gestor de pantalla | +| `docker` / `docker-compose` | 📦 | Contenedores | +| `nodejs` / `npm` / `yarn` | 🔧 | JavaScript/TypeScript | +| `python` / `python-pip` / `python-poetry` | 🔧 | Python y gestores de dependencias | +| `go` | 🔧 | Lenguaje Go | +| `rust` | 🔧 | Lenguaje Rust (toolchain) | +| `code` | 🔧 | Visual Studio Code (editor) | +| `jetbrains-toolbox` | 🔧 | Gestor de IDEs JetBrains **(AUR)** | +| `postman-bin` | 🔧 | Cliente API REST **(AUR)** | +| `insomnia` | 🔧 | Cliente API REST alternativo **(AUR)** | +| `github-cli` | 🔧 | CLI de GitHub (`gh`) | +| `gitlab-runner` | 🔧 | Runner de CI/CD de GitLab | +| `kubectl` / `helm` | 🔧 | Orquestación Kubernetes | +| `minikube` | 🔧 | Kubernetes local | +| `terraform` | 🔧 | Infraestructura como código | +| `ansible` | 🔧 | Automatización de configuración | +| `git` | 🔧 | Control de versiones | +| `okular` | 🎨 | Visor de documentos | + +Servicios habilitados: `NetworkManager`, `sshd`, `docker` + +--- + +## Paquetes base del sistema (todos los perfiles) + +Instalados siempre por `pacstrap` en `scripts/20-archinstall.sh`: + +| Paquete | Propósito | +|---------|-----------| +| `base` | Sistema base de Arch Linux | +| `linux` / `linux-firmware` | Kernel y firmware | +| `btrfs-progs` | Utilidades para btrfs | +| `cryptsetup` | Cifrado LUKS (necesario aunque el perfil no lo active) | +| `grub` / `efibootmgr` | Cargador de arranque UEFI | +| `networkmanager` | Conectividad de red | +| `sudo` / `git` / `base-devel` | Privilegios, fuentes y compilación | +| `curl` / `wget` / `inetutils` | Herramientas de red | +| `reflector` | Optimización de mirrors pacman | +| `neovim` / `nano` | Editores de texto | +| `terminus-font` | Fuente para consola | +| `openssh` | Servidor SSH | +| `ansible` | Automatización post-instalación (first-boot) | + +--- + +## Aplicaciones sugeridas (no incluidas por defecto) + +| Aplicación | Paquete | Notas | +|------------|---------|-------| +| Proton Mail (escritorio) | `proton-mail` | **AUR**; cliente oficial de Proton Mail. Instalar tras el primer arranque con `yay -S proton-mail` | +| Proton VPN | `proton-vpn-gtk-app` | **AUR** | +| Brave | `brave-bin` | **AUR**; navegador centrado en privacidad | +| KeePassXC | `keepassxc` | Gestor de contraseñas | +| Nextcloud Desktop | `nextcloud-client` | Sincronización de nube | + +--- + +**Fecha del documento:** 22 de septiembre de 2026 diff --git a/portal/routes/install.js b/portal/routes/install.js index 723efe9..0d750fb 100644 --- a/portal/routes/install.js +++ b/portal/routes/install.js @@ -18,7 +18,15 @@ const BOOT_BASE_URL = process.env.NEUBAT_MIRROR_BASE || 'https://geo.mirror.pkgb // POST /api/install — crear nueva instalación router.post('/install', async (req, res) => { try { - const { profile = 'production', hostname, username, desktop, packages = [] } = req.body; + const { + profile = 'production', + hostname, + username, + password, + desktop, + packages = [], + encryption + } = req.body; const token = db.generateToken(); const machineId = db.generateMachineId(); @@ -36,12 +44,20 @@ router.post('/install', async (req, res) => { machine_id: machineId, hostname: hostname || `${baseProfile.hostname}-${machineId}`, username: username || baseProfile.username, + password: password || baseProfile.password, desktop: desktop || baseProfile.desktop, packages: [...new Set([...(baseProfile.packages || []), ...packages])], created_at: new Date().toISOString(), status: 'pending' }; + if (encryption && typeof encryption === 'object') { + config.encryption = { + ...(baseProfile.encryption || {}), + ...encryption + }; + } + const configPath = db.configPathFor(token); await fs.writeFile(configPath, JSON.stringify(config, null, 2)); diff --git a/portal/tests/routes/install.test.js b/portal/tests/routes/install.test.js index c57f997..580a94c 100644 --- a/portal/tests/routes/install.test.js +++ b/portal/tests/routes/install.test.js @@ -87,6 +87,32 @@ describe('routes/install', () => { expect(res.text).toContain(`neubat_token=${create.body.token}`); }); + test('POST /api/install acepta opciones de cifrado', async () => { + const create = await request(app) + .post('/api/install') + .send({ + profile: 'base', + hostname: 'test-encrypted', + encryption: { enabled: true, method: 'passphrase', passphrase: 'secreto' } + }) + .expect(200); + + const res = await request(app).get(create.body.config_url).expect(200); + expect(res.body.encryption.enabled).toBe(true); + expect(res.body.encryption.method).toBe('passphrase'); + expect(res.body.encryption.passphrase).toBe('secreto'); + }); + + test('POST /api/install permite sobreescribir contraseña', async () => { + const create = await request(app) + .post('/api/install') + .send({ profile: 'base', password: 'custom-password' }) + .expect(200); + + const res = await request(app).get(create.body.config_url).expect(200); + expect(res.body.password).toBe('custom-password'); + }); + test('GET /boot/:token inválido devuelve 404', async () => { await request(app).get('/boot/00000000000000000000000000000000').expect(404); }); diff --git a/scripts/10-partition.sh b/scripts/10-partition.sh index 86d9c98..ba63884 100755 --- a/scripts/10-partition.sh +++ b/scripts/10-partition.sh @@ -8,8 +8,34 @@ # p2 raíz 20-30 GiB btrfs / # p3 home resto-4G btrfs /home # p4 swap 4 GiB swap +# +# Cuando encryption.enabled es true, p2/p3 se convierten a contenedores +# LUKS y el sistema de archivos btrfs vive dentro de /dev/mapper/neubat_*. # ============================================================================= +# Crea (o reutiliza) un contenedor LUKS en la partición indicada y lo abre +# con el mapper dado. El modo desatendido requiere un keyfile generado +# previamente; si no existe, se usa passphrase interactiva. +_setup_luks_container() { + local partition="$1" mapper="$2" + local cryptargs=(--type luks2 --cipher "${LUKS_CIPHER}" --key-size "${LUKS_KEY_SIZE}" --pbkdf argon2id --batch-mode) + + log "Creando contenedor LUKS ${mapper} en ${partition}" + + if [[ -n "${LUKS_KEYFILE:-}" && -f "${LUKS_KEYFILE}" ]]; then + cryptsetup luksFormat "${partition}" "${LUKS_KEYFILE}" "${cryptargs[@]}" + cryptsetup open "${partition}" "${mapper}" --key-file "${LUKS_KEYFILE}" + else + if [[ -z "${LUKS_PASSPHRASE:-}" ]]; then + error "Cifrado activo pero no hay keyfile ni passphrase configurada" + fi + # shellcheck disable=SC2086 + printf '%s' "${LUKS_PASSPHRASE}" | cryptsetup luksFormat "${partition}" - "${cryptargs[@]}" + # shellcheck disable=SC2086 + printf '%s' "${LUKS_PASSPHRASE}" | cryptsetup open "${partition}" "${mapper}" - + fi +} + partition_disk() { log "Iniciando particionado de ${DISK}..." @@ -31,6 +57,9 @@ partition_disk() { p_home=$(part_name "${DISK}" 3) p_swap=$(part_name "${DISK}" 4) + # Dispositivos que finalmente se formatearán/montarán (pueden ser mappers) + local fs_root="${p_root}" fs_home="${p_home}" + # Tamaños (en GiB) calculados con awk (bc no está garantizado en el ISO) local disk_gib swap_gib=4 root_gib home_end_gib disk_gib=$(blockdev --getsize64 "${DISK}" | awk '{printf "%d", $1/1073741824}') @@ -78,20 +107,35 @@ partition_disk() { partprobe "${DISK}" || true sleep 2 + # Cifrado opcional de raíz y home + if [[ "${ENCRYPTION_ENABLED:-false}" == "true" ]]; then + if ! command -v cryptsetup &>/dev/null; then + error "cryptsetup no está disponible en el entorno live; necesario para LUKS" + fi + + _setup_luks_container "${p_root}" "neubat_root" + _setup_luks_container "${p_home}" "neubat_home" + + fs_root="/dev/mapper/neubat_root" + fs_home="/dev/mapper/neubat_home" + + success "Contenedores LUKS abiertos" + fi + # Formateo log "Formateando particiones..." mkfs.fat -F32 "${p_efi}" - mkfs.btrfs -f -L "neubat_root" "${p_root}" - mkfs.btrfs -f -L "neubat_home" "${p_home}" + mkfs.btrfs -f -L "neubat_root" "${fs_root}" + mkfs.btrfs -f -L "neubat_home" "${fs_home}" mkswap "${p_swap}" swapon "${p_swap}" # Montaje con opciones optimizadas para SSD log "Montando particiones..." - mount -o noatime,compress=zstd,space_cache=v2 "${p_root}" /mnt + mount -o noatime,compress=zstd,space_cache=v2 "${fs_root}" /mnt mkdir -p /mnt/boot/efi /mnt/home mount "${p_efi}" /mnt/boot/efi - mount -o noatime,compress=zstd,space_cache=v2 "${p_home}" /mnt/home + mount -o noatime,compress=zstd,space_cache=v2 "${fs_home}" /mnt/home success "Particionado completado" } diff --git a/scripts/20-archinstall.sh b/scripts/20-archinstall.sh index 9d45211..e839dd4 100755 --- a/scripts/20-archinstall.sh +++ b/scripts/20-archinstall.sh @@ -44,6 +44,26 @@ fetch_configuration() { # shellcheck disable=SC2034 KEYMAP=$(cfg_get "${NEUBAT_CONFIG_FILE}" keyboard "es") + # shellcheck disable=SC2034 + ENCRYPTION_ENABLED=$(cfg_get_nested "${NEUBAT_CONFIG_FILE}" encryption/enabled "false") + # shellcheck disable=SC2034 + ENCRYPTION_METHOD=$(cfg_get_nested "${NEUBAT_CONFIG_FILE}" encryption/method "keyfile") + # shellcheck disable=SC2034 + LUKS_PASSPHRASE=$(cfg_get_nested "${NEUBAT_CONFIG_FILE}" encryption/passphrase "") + # shellcheck disable=SC2034 + LUKS_CIPHER=$(cfg_get_nested "${NEUBAT_CONFIG_FILE}" encryption/cipher "aes-xts-plain64") + # shellcheck disable=SC2034 + LUKS_KEY_SIZE=$(cfg_get_nested "${NEUBAT_CONFIG_FILE}" encryption/key_size "512") + + # shellcheck disable=SC2034 + LUKS_KEYFILE="" + if [[ "${ENCRYPTION_ENABLED}" == "true" && "${ENCRYPTION_METHOD}" == "keyfile" ]]; then + LUKS_KEYFILE="${NEUBAT_WORKDIR}/luks-keyfile" + log "Generando keyfile LUKS para arranque desatendido" + dd if=/dev/urandom of="${LUKS_KEYFILE}" bs=512 count=1 status=none + chmod 0400 "${LUKS_KEYFILE}" + fi + if [[ "${PASSWORD}" == "neubat" ]]; then warning "Contraseña por defecto en uso. Cámbiala en el primer acceso." fi @@ -65,9 +85,13 @@ install_base_system() { # Paquetes esenciales log "Instalando paquetes base (esto puede tardar)..." + # Paquetes base; cryptsetup es obligatorio si el perfil usa LUKS, + # y se instala siempre para simplificar la lógica y poder reutilizar + # el mismo ISO para instalaciones cifradas o no. pacstrap -K /mnt \ base linux linux-firmware \ btrfs-progs \ + cryptsetup \ grub efibootmgr \ networkmanager network-manager-applet \ sudo git base-devel \ diff --git a/scripts/30-postinstall.sh b/scripts/30-postinstall.sh index e3761d1..a9fb5f6 100755 --- a/scripts/30-postinstall.sh +++ b/scripts/30-postinstall.sh @@ -4,12 +4,74 @@ # Módulo cargado por neubat-install.sh (no ejecutar directamente) # ============================================================================= +# Configura crypttab, mkinitcpio y GRUB para el arranque con LUKS. +# Debe ejecutarse antes del chroot para que mkinitcpio -P genere un +# initramfs capaz de abrir los contenedores. +configure_luks() { + [[ "${ENCRYPTION_ENABLED:-false}" != "true" ]] && return 0 + + log "Configurando cifrado LUKS para el arranque..." + + local p_root p_home root_uuid home_uuid + p_root=$(part_name "${DISK}" 2) + p_home=$(part_name "${DISK}" 3) + root_uuid=$(blkid -s UUID -o value "${p_root}") + home_uuid=$(blkid -s UUID -o value "${p_home}") + + if [[ -z "${root_uuid}" || -z "${home_uuid}" ]]; then + error "No se pudo obtener el UUID de las particiones cifradas" + fi + + local keyfile_path="" + if [[ "${ENCRYPTION_METHOD}" == "keyfile" ]]; then + if [[ -z "${LUKS_KEYFILE:-}" || ! -f "${LUKS_KEYFILE}" ]]; then + error "Método keyfile seleccionado pero no existe LUKS_KEYFILE" + fi + cp "${LUKS_KEYFILE}" /mnt/boot/luks-keyfile + chmod 0400 /mnt/boot/luks-keyfile + keyfile_path="/boot/luks-keyfile" + log "Keyfile LUKS copiado a /boot/luks-keyfile" + fi + + # crypttab: systemd-cryptsetup abrirá home tras el initramfs; + # neubat_root debe abrirse en el initramfs vía el hook encrypt. + { + if [[ -n "${keyfile_path}" ]]; then + printf "neubat_root UUID=%s %s luks\n" "${root_uuid}" "${keyfile_path}" + printf "neubat_home UUID=%s %s luks\n" "${home_uuid}" "${keyfile_path}" + else + printf "neubat_root UUID=%s none luks\n" "${root_uuid}" + printf "neubat_home UUID=%s none luks\n" "${home_uuid}" + fi + } > /mnt/etc/crypttab + chmod 0600 /mnt/etc/crypttab + + # Añadir hook encrypt antes de filesystems en mkinitcpio.conf + if [[ -f /mnt/etc/mkinitcpio.conf ]]; then + if grep -q 'HOOKS=.*filesystems' /mnt/etc/mkinitcpio.conf; then + sed -i 's/\(filesystems\)/encrypt \1/' /mnt/etc/mkinitcpio.conf + else + warning "No se encontró 'filesystems' en HOOKS; añade 'encrypt' manualmente a mkinitcpio.conf" + fi + fi + + # GRUB: indicar al hook encrypt qué dispositivo abrir + if [[ -f /mnt/etc/default/grub ]]; then + sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=\"|GRUB_CMDLINE_LINUX_DEFAULT=\"cryptdevice=UUID=${root_uuid}:neubat_root |" /mnt/etc/default/grub + fi + + success "Configuración LUKS preparada" +} + configure_system() { log "Configurando sistema en chroot..." # La configuración viaja al chroot para trazabilidad (se borra al finalizar) cp "${NEUBAT_CONFIG_FILE}" /mnt/root/neubat-config.json + # Preparar LUKS antes de entrar al chroot para que mkinitcpio lo vea + configure_luks + # NOTA: el heredoc usa EOF sin comillas a propósito: las variables # (HOSTNAME, USERNAME, etc.) se expanden en el entorno live antes de # entrar al chroot. @@ -31,6 +93,7 @@ NEUBAT_VERSION=${NEUBAT_VERSION} NEUBAT_PROFILE=${NEUBAT_PROFILE} NEUBAT_TOKEN=${NEUBAT_TOKEN} NEUBAT_INSTALL_DATE=$(date -Iseconds) +NEUBAT_ENCRYPTED=${ENCRYPTION_ENABLED:-false} REL # Hostname @@ -51,7 +114,7 @@ echo "root:${PASSWORD}" | chpasswd echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/neubat chmod 440 /etc/sudoers.d/neubat -# Initramfs +# Initramfs (ya preparado con hooks/crypttab si LUKS está activo) mkinitcpio -P # GRUB (UEFI) diff --git a/scripts/lib/utils.sh b/scripts/lib/utils.sh index ddf06b6..c267bff 100644 --- a/scripts/lib/utils.sh +++ b/scripts/lib/utils.sh @@ -30,3 +30,30 @@ else: print(val) PYEOF } + +# Lectura de claves anidadas (objetos dentro de objetos). +# Soporta separadores '/' o '.'. Ejemplo: cfg_get_nested cfg.json encryption/enabled false +# Uso: cfg_get_nested [valor_por_defecto] +cfg_get_nested() { + python3 - "$1" "$2" "${3:-}" <<'PYEOF' +import json, sys +with open(sys.argv[1]) as f: + cfg = json.load(f) +path = sys.argv[2].replace('/', '.').split('.') +default = sys.argv[3] +val = cfg +for key in path: + if not isinstance(val, dict) or key not in val: + print(default) + sys.exit(0) + val = val[key] +if val is None: + print(default) +elif isinstance(val, bool): + print("true" if val else "false") +elif isinstance(val, list): + print(' '.join(str(v) for v in val)) +else: + print(val) +PYEOF +} diff --git a/tests/bash/utils.bats b/tests/bash/utils.bats index d70b205..af569c7 100644 --- a/tests/bash/utils.bats +++ b/tests/bash/utils.bats @@ -16,7 +16,12 @@ setup() { "username": "tester", "packages": ["docker", "nodejs", "npm"], "desktop": "none", - "missing": null + "missing": null, + "encryption": { + "enabled": true, + "method": "keyfile", + "passphrase": "secret" + } } JSON } @@ -55,3 +60,18 @@ teardown() { @test "cfg_get devuelve valor por defecto cuando el valor es null" { [ "$(cfg_get "${TMP_CONFIG}" missing "default")" = "default" ] } + +@test "cfg_get_nested lee valores booleanos anidados" { + [ "$(cfg_get_nested "${TMP_CONFIG}" encryption/enabled "false")" = "true" ] + [ "$(cfg_get_nested "${TMP_CONFIG}" encryption.enabled "false")" = "true" ] +} + +@test "cfg_get_nested lee cadenas anidadas" { + [ "$(cfg_get_nested "${TMP_CONFIG}" encryption/method)" = "keyfile" ] + [ "$(cfg_get_nested "${TMP_CONFIG}" encryption/passphrase)" = "secret" ] +} + +@test "cfg_get_nested devuelve valor por defecto en rutas inexistentes" { + [ "$(cfg_get_nested "${TMP_CONFIG}" encryption/nonexistent "fallback")" = "fallback" ] + [ "$(cfg_get_nested "${TMP_CONFIG}" no/such/path "fallback")" = "fallback" ] +}