Índice / Módulo 2 — Hardening del operador

2.99 Instalación y endurecimiento de Arch Linux

Guía exhaustiva para endurecer un puesto de trabajo ofensivo basado en Arch Linux. Todas las decisiones técnicas están tomadas, los conflictos entre seguridad y usabilidad resueltos, y las excepciones documentadas para que sepas qué sacrificas y por qué. Se ha usado un ordenador portátil ASUS Zenbook para la prueba. El uso de otro modelo puede variar mínimamente algunos de los comandos usados.

2.99.1 Lo que conseguirás al final de esta guía

Cuando termines, tu portátil será esto:

  • Un sistema que solo arranca con lo que tú has firmado: Secure Boot con claves propias, nada de bootkits ni firmas de terceros.
  • Un disco completamente cifrado, con el TPM sellado y un PIN para el desbloqueo; si te lo roban apagado, los datos no se leen.
  • Apagado automático al cerrar la tapa: ni suspensión ni hibernación que dejen la clave en RAM.
  • Lockdown del kernel en modo integridad: ni siquiera root puede modificar el kernel en caliente.
  • Firewall con política DROP en entrada, sin puertos abiertos, y con reglas que no rompen tus VMs ni contenedores.
  • Podman rootless en lugar de Docker, sin demonio privilegiado, sin reglas de firewall saltadas.
  • Herramientas ofensivas en una VM desechable: el host se mantiene limpio, y restauras el estado base en segundos.
  • Logs sellados, auditoría y canarios: si alguien entra, te enteras, y si borra algo, lo sabes.
  • Copias de seguridad cifradas y pruebas de restauración: no te quedas sin salida si el TPM falla o el disco se corrompe.
  • Un script de verificación final que comprueba todo esto en un solo comando.

No es un sistema de usar y tirar. Es una máquina de trabajo ofensivo que no sacrifica la seguridad por la comodidad, pero tampoco se vuelve inservible por aplicar hardening a lo loco. Las excepciones están documentadas y justificadas.

2.99.2 Decisiones cerradas

Estas son las decisiones que definen la instalación. Son elecciones de configuración y uso, no características del hardware. Si alguna no encaja con tu forma de trabajar, es mejor que lo sepas antes de continuar.

  • Kernel: linux-hardened como kernel principal y linux como respaldo. Ambos firmados para Secure Boot.
  • Secure Boot: claves propias generadas con sbctl, con posibilidad de añadir certificados de Microsoft para compatibilidad con el firmware.
  • TPM: desbloqueo de LUKS sellado a PCR 7, con PIN adicional obligatorio.
  • Cifrado: disco completo con LUKS2, sin swap en disco. La swap va en zram dentro de la RAM.
  • Al cerrar la tapa: apagado completo. Sin suspensión ni hibernación.
  • Escritorio: GNOME sobre Wayland, con la sesión de X11 deshabilitada.
  • Red: solo Wi-Fi, gestionado con iwd. Sin servicios de red accesibles desde el exterior.
  • SSH entrante: no se instala. No hay ningún servicio escuchando en la red.
  • Virtualización: KVM con libvirt para las máquinas virtuales. Módulos firmados y compatibles con lockdown.
  • Contenedores: Podman en modo rootless. Docker queda descartado por su demonio privilegiado y su gestión del firewall.
  • Herramientas ofensivas: se ejecutan en una VM dedicada, nunca en el host. El host se mantiene limpio y actualizable.
  • Compilación: no se instala base-devel en el host. Las compilaciones puntuales se hacen en un contenedor desechable.
  • Firewall: nftables con política DROP en entrada y reglas explícitas para las redes internas de VMs y contenedores.
  • AppArmor: activo y con perfiles en modo enforce para las aplicaciones con más exposición.
  • Auditoría: auditd con reglas de integridad y canarios para detectar accesos no autorizados.
  • Copias de seguridad: cifradas con Borg y almacenadas fuera del equipo. La restauración se prueba trimestralmente.

Estas decisiones implican renuncias concretas. Son intencionadas y están documentadas para que sepas qué estás sacrificando:

  • Sin suspensión: al cerrar la tapa el equipo se apaga. No hay suspensión a RAM ni hibernación, por lo que se pierde la capacidad de reanudar el trabajo instantáneamente.
  • Sin drivers propietarios ni módulos DKMS: el kernel firmado y el lockdown impiden cargar módulos no firmados. Los drivers de NVIDIA o módulos de terceros que requieran DKMS no funcionarán.
  • Sin compilación directa en el host: no se instala base-devel ni gcc en el sistema principal. Cualquier compilación debe realizarse dentro de un contenedor desechable.

Si necesitas alguna de estas capacidades, existen alternativas documentadas en los bloques correspondientes: usar una máquina virtual, un contenedor de compilación o reconfigurar el arranque con otra política de suspensión (asumiendo los riesgos).

2.99.3 Firmware ASUS

Este bloque actualiza el firmware del equipo y deja la configuración UEFI preparada para todo lo que viene después. Es un paso previo obligatorio: sin él, Secure Boot, el TPM y la virtualización no funcionarán como se espera.

El firmware de fábrica suele estar desactualizado y puede arrastrar vulnerabilidades de microcódigo. Una forma rápida de comprobarlo es ejecutar, desde cualquier Linux live:

bash
cat /sys/devices/system/cpu/vulnerabilities/tsa

Si aparece Vulnerable: No microcode, el sistema no está aplicando las correcciones más recientes para Transient Scheduler Attacks (TSA), una familia de fallos de canal lateral de AMD. La solución tiene dos partes: actualizar el firmware y cargar el microcódigo adecuado, que se instalará más adelante con el paquete amd-ucode.

3.1 Actualizar el firmware

Hay dos vías para actualizar el firmware. Prueba primero la automática:

  1. Desde un sistema Linux con acceso a internet, instala fwupd y consulta las actualizaciones disponibles:

    bash
    sudo apt install fwupd
    sudo fwupdmgr refresh --force
    sudo fwupdmgr get-updates
  2. Si no aparece ninguna actualización, ve a la web de soporte del fabricante, busca el modelo del equipo, y descarga la versión más reciente del firmware.

  3. Descomprime el archivo descargado y copia el fichero de firmware a un USB formateado en FAT32.

  4. Reinicia y pulsa ESC para entrar en la configuración UEFI.

  5. Activa el modo avanzado con F7, ve a la pestaña Advanced y selecciona la utilidad de actualización de firmware (habitualmente llamada EZ Flash en equipos ASUS).

  6. Selecciona el fichero del USB. No apagues el equipo durante el proceso, se reiniciará solo varias veces.

  7. Al terminar, entra de nuevo en la UEFI y confirma que la versión instalada es la nueva.

3.2 Configurar el UEFI

Reinicia y entra otra vez con ESC y F7. Aplica los siguientes ajustes en este orden:

UbicaciónAjusteValorMotivo
AdvancedSVM ModeEnabledActiva la virtualización por hardware para KVM
AdvancedTrusted Computing → Security Device SupportEnabledHabilita el fTPM, necesario para el sellado
AdvancedTrusted Computing → Pending OperationTPM ClearDeja el TPM en estado limpio antes de usarlo
AdvancedNetwork Stack Configuration → Network StackDisabledReduce superficie de ataque en el arranque
AdvancedUSB Configuration → USB Mass Storage Driver SupportEnabled por ahoraNecesario para arrancar desde USB; se desactiva al final
BootFast BootDisabledEvita que el firmware se salte pasos de arranque y mantiene estables las entradas EFI
SecuritySecure BootDisabled por ahoraSe activará después de firmar los binarios
SecurityUSB Interface Security → BluetoothLOCKDesactiva el Bluetooth. Si lo necesitas para raton/teclado dejalo activado.
SecurityUSB Interface Security → CMOS CameraLOCKDesactiva la cámara integrada
SecurityAdministrator PasswordEstablécela al final de esta configuraciónProtege la configuración UEFI

Mantén activo el arranque desde USB. Lo desactivaremos al final de la guía.

⚠️
Establece la contraseña de administrador al final de esta configuración UEFI. En algunos equipos, una vez puesta, el menú de gestión de claves queda en modo solo lectura y obliga a resetear la NVRAM para recuperar el control. Además, en este modelo concreto, es posible que el submenú Key Management solo aparezca después de poner la contraseña.
💡
Sobre el fTPM de AMD: es firmware, no un chip dedicado. Un Clear CMOS o ciertas actualizaciones de AGESA pueden resetearlo y provocar la pérdida del sellado. Por eso la clave de recuperación no es opcional. Algunas versiones antiguas de AGESA también causaban microcortes de audio o del puntero; si aparecen tras actualizar, es un síntoma conocido.

3.3 Comprobación

Arranca desde el USB de Arch y ejecuta estas verificaciones:

bash
(iso)# ls /sys/firmware/efi/efivars                 # debe aparecer una lista
(iso)# ls /dev/tpmrm0                               # debe aparecer una linea
(iso)# ls /sys/kernel/iommu_groups | wc -l          # debe ser mayor que 0
(iso)# bootctl status | grep -i "secure boot"       # debe mostrar setup o disabled
(iso)# grep -c svm /proc/cpuinfo                    # debe ser mayor que 0
(iso)# cat /sys/devices/system/cpu/vulnerabilities/tsa

Qué comprueba cada comando:

  • ls /sys/firmware/efi/efivars: lista las variables EFI del firmware. Si el directorio existe y tiene contenido, el sistema ha arrancado en modo UEFI nativo y no en modo legacy (BIOS). Sin esto, Secure Boot no es posible.
  • ls /dev/tpmrm0: comprueba si el TPM 2.0 está detectado y expuesto como dispositivo de acceso. Si aparece una línea con ese nombre, el fTPM está activo y el sellado de LUKS podrá funcionar más adelante.
  • ls /sys/kernel/iommu_groups | wc -l: cuenta los grupos IOMMU que el kernel ha detectado. Un número mayor que cero indica que el IOMMU está activo y que se podrá aislar dispositivos DMA, tanto para seguridad como para pasar hardware a una VM.
  • bootctl status | grep -i "secure boot": consulta el estado de Secure Boot según systemd-boot y filtra solo la línea relevante. En este punto debería decir setup (modo configuración) o disabled, porque todavía no hemos firmado nada ni activado Secure Boot.
  • grep -c svm /proc/cpuinfo: busca cuántas veces aparece la cadena svm en la información del procesador. SVM (Secure Virtual Machine) es el nombre que AMD da a su extensión de virtualización por hardware; un número mayor que cero confirma que está habilitada en BIOS.
  • cat /sys/devices/system/cpu/vulnerabilities/tsa: lee el estado de mitigación para la vulnerabilidad TSA (Transient Scheduler Attacks). Si dice Vulnerable: No microcode, falta actualizar firmware o cargar microcódigo; si dice Mitigation: Clear CPU buffers, el kernel ya está aplicando la mitigación completa.

2.99.4 Verificar el medio de instalación

Antes de escribir la ISO en el USB conviene verificar que la descarga es íntegra y auténtica. Una ISO corrupta puede provocar fallos difíciles de diagnosticar durante la instalación; una ISO manipulada es un vector de ataque real. La verificación tiene dos partes: comprobar el hash SHA256 para descartar corrupción, y verificar la firma GPG para confirmar que el archivo procede de Arch Linux y no ha sido alterado.

4.1 Descargar la ISO y los archivos de verificación

Desde un sistema Linux con conexión a internet, descarga los tres archivos necesarios:

bash
curl -O https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso
curl -O https://archlinux.org/iso/latest/archlinux-x86_64.iso.sig
curl -O https://archlinux.org/iso/latest/sha256sums.txt

Qué hace cada uno:

  • curl -O: descarga un archivo desde una URL y lo guarda con el mismo nombre que tiene en el servidor. La opción -O (mayúscula) es la que fuerza el nombre original; sin ella, el contenido se imprimiría en la terminal.
  • La primera URL apunta a un mirror de Arch; las otras dos, al servidor oficial, que es donde se publican la firma y las sumas de verificación.

4.2 Verificar la integridad

Comprueba que el hash de la ISO descargada coincide con el publicado:

bash
sha256sum -c sha256sums.txt --ignore-missing

Qué hace:

  • sha256sum -c: modo "check". Lee el archivo de sumas y, para cada entrada, calcula el hash del archivo correspondiente y lo compara con el valor publicado.
  • --ignore-missing: si el archivo de sumas incluye entradas para ISOs que no has descargado, las omite en lugar de marcar error. Solo se verifica lo que tienes en disco.

Debe aparecer archlinux-x86_64.iso: OK. Si el resultado es FAILED, la descarga está corrupta o ha sido manipulada; vuelve a descargarla desde otro mirror.

4.3 Verificar la autenticidad

El hash confirma integridad, pero no autenticidad: alguien que controle el mirror podría publicar una ISO modificada junto con su hash correspondiente. Para descartarlo, se verifica la firma GPG contra la clave del desarrollador que firmó la release.

bash
gpg --auto-key-locate clear,wkd --locate-external-key pierre@archlinux.org
gpg --verify archlinux-x86_64.iso.sig archlinux-x86_64.iso

Qué hace cada uno:

  • gpg --auto-key-locate clear,wkd --locate-external-key: busca y descarga automáticamente la clave pública asociada a un correo. clear indica que se use el servidor de claves directo si está disponible; wkd (Web Key Directory) permite que la clave se obtenga desde el dominio del correo. Es una forma de no tener que importar la clave manualmente.
  • gpg --verify: comprueba que la firma del archivo .sig corresponde realmente a la ISO descargada y que fue emitida por la clave privada del desarrollador. Si la ISO hubiera sido modificada, la firma no validaría.

La salida debe incluir Good signature from "Pierre Schmitz". Un aviso de "clave no certificada" es normal si no has firmado la clave del desarrollador con tu propia clave; lo que no es aceptable es un BAD signature.

4.4 Escribir la ISO al USB

Con la ISO verificada, escríbela al dispositivo USB. Asegúrate de identificar correctamente el dispositivo para no sobrescribir el disco del sistema:

bash
lsblk
sudo dd if=archlinux-x86_64.iso of=/dev/sdX bs=4M status=progress oflag=sync

Qué hace:

  • lsblk: lista los dispositivos de bloque conectados (discos, particiones, USB). Sirve para identificar qué nombre tiene tu USB antes de escribir sobre él.
  • dd if=: especifica el archivo de entrada; aquí, la ISO.
  • of=: especifica el dispositivo de salida; aquí, el USB. Este es el parámetro crítico: si pones por error el disco principal, lo destruyes.
  • bs=4M: tamaño de bloque. 4 MB es un buen equilibrio entre velocidad y uso de memoria.
  • status=progress: muestra por pantalla el progreso de la escritura, en lugar de quedarse en silencio hasta terminar.
  • oflag=sync: fuerza a que los datos se escriban físicamente en el dispositivo antes de dar la operación por terminada. Sin esto, podrías extraer el USB antes de que se complete la escritura real.
⚠️
Verifica el dispositivo antes de ejecutar dd. Un error en la ruta de of= destruye el disco que escribas sin posibilidad de recuperación. Comprueba con lsblk que /dev/sdX es realmente el USB y no tu disco principal.

2.99.5 Arranque live y conexión de red

Con la ISO verificada y el USB preparado, el siguiente paso es arrancar el sistema live de Arch y establecer conexión a internet. Sin red no es posible instalar: pacstrap descarga los paquetes desde los repositorios oficiales.

Este equipo no tiene puerto Ethernet, así que la conexión se hace obligatoriamente por Wi-Fi. Si en tu caso dispones de Ethernet, puedes saltarte la parte del cliente Wi-Fi y conectar el cable directamente.

5.1 Arrancar desde el USB

Apaga el equipo, conecta el USB y enciéndelo pulsando la tecla de menú de arranque (habitualmente ESC o F11 en equipos ASUS). Selecciona el USB en la lista de dispositivos de arranque.

Cuando aparezca el menú de systemd-boot del live, selecciona la primera entrada (arranque normal). Transcurridos unos segundos llegarás a un prompt de root con el identificador root@archiso.

5.2 Configurar el teclado

El live arranca con la distribución de teclado en inglés. Si vas a escribir contraseñas con caracteres propios del español (ñ, acentos, símbolos como ¿), cámbiala antes de nada:

bash
loadkeys es

Qué hace:

  • loadkeys es: carga la distribución de teclado española en la consola actual. Se aplica solo a la sesión de terminal en la que se ejecuta, no es persistente. Si el live se reinicia, hay que repetirlo.
💡
Verificación rápida: después de cargar el teclado español, escribe ñ y comprueba que aparece correctamente. Si sale un carácter distinto, la distribución no se ha aplicado; revisa que no haya un error tipográfico en el nombre (es, no esp ni spanish).

5.3 Conectar por Wi-Fi con iwd

El live de Arch incluye iwd (iNet Wireless Daemon) como cliente Wi-Fi. Es más ligero y moderno que wpa_supplicant, y es el mismo que usaremos después en el sistema instalado, así que conviene familiarizarse con él desde ahora.

Inicia el modo interactivo de iwd:

bash
iwctl

Dentro del prompt de iwd ejecuta los siguientes comandos en orden:

bash
[iwd]# device list
[iwd]# station wlan0 scan
[iwd]# station wlan0 get-networks
[iwd]# station wlan0 connect NOMBRE_DE_TU_RED
[iwd]# exit

Qué hace cada uno:

  • device list: muestra las interfaces de red inalámbricas detectadas. Lo habitual es que aparezca como wlan0, pero podría ser otro nombre.
  • station wlan0 scan: ordena a la interfaz wlan0 que escanee las redes Wi-Fi disponibles en el entorno. El escaneo puede tardar unos segundos.
  • station wlan0 get-networks: lista las redes detectadas por el escaneo anterior, con su SSID y nivel de señal.
  • station wlan0 connect NOMBRE_DE_TU_RED: inicia la conexión a la red indicada. Si la red usa cifrado WPA2 o WPA3, pedirá la contraseña de forma interactiva.
  • exit: sale del cliente iwd y vuelve al shell normal del live.

Alternativa en una sola línea, sin entrar en el modo interactivo:

bash
iwctl station wlan0 connect NOMBRE_DE_TU_RED

5.4 Verificar la conexión

Antes de continuar, confirma que hay conectividad real hacia el exterior:

bash
ping -c3 archlinux.org

Qué hace:

  • ping: envía paquetes ICMP a un destino para comprobar si hay conectividad. Es la prueba más básica de "¿hay internet?".
  • -c3: envía solo 3 paquetes y termina. Sin esta opción, ping se queda en ejecución indefinidamente.

La salida debe mostrar líneas con tiempos de respuesta (time=XX ms) y terminar con un resumen. Si todas las peticiones se pierden, la red no está bien configurada: revisa el SSID, la contraseña y que el adaptador Wi-Fi esté detectado.

5.5 Sincronizar el reloj del sistema

Arch verifica las firmas GPG de los paquetes durante la instalación. Si el reloj del sistema está muy desviado, las firmas aparecerán como inválidas y la instalación fallará con errores criptográficos difíciles de interpretar. Sincroniza el reloj antes de continuar:

bash
timedatectl set-ntp true

Qué hace:

  • timedatectl set-ntp true: activa la sincronización automática del reloj mediante NTP (Network Time Protocol). El sistema consulta servidores de tiempo públicos y ajusta la hora local. En el live se aplica solo durante la sesión, pero el sistema instalado conservará esta configuración si dejamos el servicio habilitado.

Verifica que la hora es correcta:

bash
timedatectl status

5.6 Si la interfaz Wi-Fi no aparece

Si el comando device list dentro de iwd no muestra ninguna interfaz inalámbrica, o si station wlan0 scan devuelve error, comprueba lo siguiente:

bash
dmesg | grep -i iwlwifi
rfkill list
rfkill unblock all

Qué hace cada uno:

  • dmesg | grep -i iwlwifi: busca en el registro del kernel las líneas relacionadas con el driver iwlwifi, que es el que gobierna los adaptadores Intel. Si no hay ninguna línea, el kernel no ha detectado el hardware.
  • rfkill list: muestra el estado de los bloqueos por software y hardware de los dispositivos de radio (Wi-Fi, Bluetooth). Si alguna línea aparece como Soft blocked: yes, el Wi-Fi está bloqueado por software.
  • rfkill unblock all: desbloquea todos los dispositivos de radio. Es lo habitual cuando el Wi-Fi aparece como bloqueado en un equipo nuevo o tras haber estado deshabilitado en Windows o en otra distribución.

Si tras desbloquear sigue sin aparecer la interfaz, comprueba que la BIOS no tiene deshabilitado el adaptador Wi-Fi. En algunos equipos ASUS, la opción Wireless Network Interface dentro de I/O Interface Security controla si el Wi-Fi está activo a nivel de firmware.

2.99.6 Particionado, cifrado y subvolúmenes

Este es el punto de no retorno de la instalación. Se van a borrar todas las particiones del disco y se va a crear desde cero la estructura sobre la que vivirá el sistema: una partición EFI, un volumen cifrado con LUKS2 que ocupa el resto del disco, y dentro de él varios subvolúmenes Btrfs con opciones de montaje restrictivas.

🚨
Este paso destruye todos los datos del disco seleccionado sin posibilidad de recuperación. Comprueba dos veces que trabajas sobre el disco correcto antes de ejecutar cualquier comando. Si tienes dudas sobre cuál es el nombre del disco, ejecuta lsblk y revisa tamaños y modelos.

6.1 Variables de trabajo

Para no repetir rutas largas en cada comando, defínelas como variables de shell en la sesión actual:

bash
DISK=/dev/nvme0n1
ESP=/dev/nvme0n1p1
LUKSPART=/dev/nvme0n1p2

Qué hace cada una:

  • DISK: ruta del disco completo. En un equipo con NVMe es /dev/nvme0n1; en un disco SATA sería /dev/sda.
  • ESP: primera partición, donde irá la partición EFI.
  • LUKSPART: segunda partición, que albergará el volumen cifrado.

Las variables solo existen en la sesión de shell actual. Si cierras la terminal o reinicias, hay que volver a definirlas.

6.2 Borrado previo del disco

Antes de crear la nueva estructura, conviene dejar el disco limpio. En un NVMe, el comando blkdiscard ordena al propio controlador que descarte todos los bloques, lo cual es rápido y suficiente para dejarlo preparado:

bash
blkdiscard $DISK

Qué hace:

  • blkdiscard: envía la orden TRIM/DISCARD al dispositivo, indicando al controlador que todos los bloques están libres. No escribe ceros; simplemente le dice al disco que olvide lo que había. En NVMe es instantáneo.
💡
Alternativa paranoica: si quieres garantizar que los datos anteriores no son recuperables ni siquiera con herramientas forenses, puedes sobrescribir todo el disco con datos aleatorios. Es mucho más lento y en un SSD no aporta ventajas frente al borrado por TRIM. Se haría así:
bash
cryptsetup open --type plain -d /dev/urandom $DISK tmpwipe
dd if=/dev/zero of=/dev/mapper/tmpwipe bs=1M status=progress
cryptsetup close tmpwipe

6.3 Crear las particiones

La estructura es simple: una partición EFI de 1 GiB y una segunda partición que ocupa el resto del disco para el volumen cifrado.

bash
sgdisk --zap-all $DISK
sgdisk -n 1:0:+1G -t 1:ef00 -c 1:A $DISK
sgdisk -n 2:0:0 -t 2:8309 -c 2:A $DISK
partprobe $DISK
lsblk $DISK

Qué hace cada uno:

  • sgdisk --zap-all: borra todas las tablas de particiones (MBR y GPT) del disco. Deja el disco como si acabara de salir de fábrica.
  • sgdisk -n 1:0:+1G: crea la primera partición. El 0 indica que empiece en el primer bloque libre; +1G que tenga 1 GiB.
  • -t 1:ef00: asigna a la partición 1 el tipo EFI System (código 0xEF00 en GPT). Es el tipo que espera el firmware para arrancar en modo UEFI.
  • -c 1:"EFI System": pone nombre a la partición, útil para identificarla después.
  • sgdisk -n 2:0:0: crea la segunda partición desde el primer bloque libre hasta el final del disco (el segundo 0 significa "hasta el final").
  • -t 2:8309: tipo Linux LUKS (código 8309). No es estrictamente necesario, pero ayuda a que otras herramientas reconozcan el contenido.
  • partprobe $DISK: pide al kernel que relea la tabla de particiones, para que los nuevos dispositivos aparezcan en /dev sin reiniciar.
  • lsblk $DISK: muestra el resultado para verificar que las dos particiones existen y tienen los tamaños esperados.

Por qué 1 GiB en la partición EFI. Muchas guías recomiendan 300 o 512 MiB, y es suficiente para una instalación típica. En este caso no: vamos a tener cuatro Unified Kernel Images (UKI) de unos 150 MiB cada una, más una copia de respaldo en EFI/BOOT. Con 300 MiB te quedarías sin espacio en la primera actualización del kernel, y eso con Secure Boot activo significa no arrancar.

6.4 Cifrar la partición con LUKS2

La segunda partición va a contener un volumen LUKS2 que cifra todo lo que haya dentro. LUKS2 es el formato moderno, con soporte para Argon2id como función de derivación de claves, más resistente a ataques con GPU que los formatos anteriores.

bash
cryptsetup luksFormat \
    --type luks2 \
    --cipher aes-xts-plain64 \
    --key-size 512 \
    --hash sha512 \
    --pbkdf argon2id \
    --iter-time 5000 \
    --use-random \
    --verify-passphrase \
    $LUKSPART

    # Puedes escribirlo en una sola linea sin poner los \
    # aqui se usan para facilitar su lectura.

Qué hace cada opción:

  • --type luks2: usa el formato LUKS2 en lugar del antiguo LUKS1. Permite Argon2id como función de derivación y tokens como el de TPM, que usaremos más adelante.
  • --cipher aes-xts-plain64: cifra con AES en modo XTS. Es el modo recomendado para discos completos.
  • --key-size 512: clave de 512 bits, que en XTS se divide en dos claves de 256 bits (una para cifrar, otra para el tweak).
  • --hash sha512: función de hash usada internamente para verificaciones de cabecera.
  • --pbkdf argon2id: función de derivación de la contraseña. Argon2id está diseñado para resistir ataques con hardware especializado (GPU, ASIC).
  • --iter-time 5000: calibra el coste de derivación para que tarde aproximadamente 5 segundos en el equipo. Eso frena ataques por fuerza bruta.
  • --use-random: usa /dev/random como fuente de entropía. Más lento pero más seguro que /dev/urandom.
  • --verify-passphrase: pide la contraseña dos veces, para evitar errores tipográficos al crearla.

La contraseña que introduzcas ahora es la contraseña maestra de cifrado del disco. Es la clave de recuperación que usarás cuando el TPM pierda el sellado, algo que ocurrirá antes o después (por ejemplo, tras una actualización de firmware). Elige una contraseña larga y memorizable, y anótala también en papel.

Abre el volumen cifrado para poder trabajar sobre él:

bash
cryptsetup open $LUKSPART cryptroot

Qué hace:

  • cryptsetup open: descifra la cabecera y crea un dispositivo mapeado en /dev/mapper/cryptroot que expone el contenido descifrado al sistema. Todo lo que se escriba en /dev/mapper/cryptroot se cifrará automáticamente antes de llegar al disco físico.
💡
Rendimiento: los procesadores modernos incluyen instrucciones AES-NI aceleradas por hardware. El cifrado completo del disco no introduce una penalización perceptible en el uso diario. En un SSD NVMe la diferencia de rendimiento es insignificante.

6.5 Crear los sistemas de ficheros

La partición EFI se formatea en FAT32 (obligatorio para que el firmware la reconozca), y el volumen descifrado se formatea en Btrfs:

bash
mkfs.fat -F32 -n ESP $ESP
mkfs.btrfs -L arch /dev/mapper/cryptroot

Qué hace cada uno:

  • mkfs.fat -F32: formatea en FAT32. El firmware UEFI solo entiende FAT32 en la partición EFI, sin excepciones.
  • -n ESP: asigna la etiqueta "ESP" a la partición.
  • mkfs.btrfs -L arch: crea un sistema de ficheros Btrfs con etiqueta "arch" sobre el volumen cifrado.

6.6 Crear los subvolúmenes Btrfs

Btrfs permite crear subvolúmenes dentro del mismo sistema de ficheros. Cada subvolumen es como una partición independiente a efectos de opciones de montaje, snapshots y organización, pero comparte el espacio total. Vamos a crear siete:

bash
mount /dev/mapper/cryptroot /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@var
btrfs subvolume create /mnt/@varlog
btrfs subvolume create /mnt/@snapshots
btrfs subvolume create /mnt/@containers
btrfs subvolume create /mnt/@libvirt
umount /mnt

Qué hace cada uno:

  • mount: monta el volumen Btrfs recién creado en /mnt para poder trabajar dentro de él.
  • btrfs subvolume create: crea cada subvolumen en la raíz del sistema de ficheros. Los nombres que empiezan por @ son una convención habitual en Arch para distinguir subvolúmenes de directorios normales.
  • umount /mnt: desmonta el volumen para poder montarlo de nuevo con las opciones correctas.

Para qué sirve cada subvolumen:

  • @: raíz del sistema. Todo lo que no vaya a otro subvolumen vive aquí.
  • @home: directorios personales. Separarlo permite hacer snapshots sin tocar los datos del sistema.
  • @var: datos variables del sistema. Separado para no contaminar los snapshots de la raíz.
  • @varlog: logs del sistema. Separado porque se monta con noexec y para poder conservar los logs aunque se restaure un snapshot de @var.
  • @snapshots: ubicación de los snapshots. Fuera de la raíz para que un snapshot no contenga a sus propios snapshots.
  • @containers: almacenamiento de Podman. Separado para poder darle opciones de montaje específicas.
  • @libvirt: imágenes de máquinas virtuales. Separado por el mismo motivo.

6.7 Montar los subvolúmenes con opciones restrictivas

Ahora se montan todos los subvolúmenes en sus rutas definitivas, cada uno con opciones adaptadas a su función. Antes de montar los anidados hay que crear los puntos de montaje dentro del subvolumen que los contiene, porque un montaje oculta el contenido previo del directorio sobre el que se monta.

bash
OPTS=noatime,compress=zstd:1,ssd,discard=async

# 1. Montar la raiz y crear los puntos de montaje de primer nivel
mount -o $OPTS,subvol=@ /dev/mapper/cryptroot /mnt
mkdir -p /mnt/{home,var,efi,tmp}
mkdir -p /mnt/.snapshots

# 2. Montar @var temporalmente para crear dentro sus subdirectorios
mount -o $OPTS,subvol=@var /dev/mapper/cryptroot /mnt/var
mkdir -p /mnt/var/log
mkdir -p /mnt/var/lib/{containers,libvirt}
umount /mnt/var

# 3. Montar todos los subvolumenes en su sitio definitivo
mount -o $OPTS,subvol=@home,nosuid,nodev                    /dev/mapper/cryptroot /mnt/home
mount -o $OPTS,subvol=@var,nosuid,nodev                     /dev/mapper/cryptroot /mnt/var
mount -o $OPTS,subvol=@varlog,nosuid,nodev,noexec           /dev/mapper/cryptroot /mnt/var/log
mount -o $OPTS,subvol=@snapshots                            /dev/mapper/cryptroot /mnt/.snapshots
mount -o $OPTS,subvol=@containers,nodev                     /dev/mapper/cryptroot /mnt/var/lib/containers
mount -o noatime,subvol=@libvirt,nosuid,nodev               /dev/mapper/cryptroot /mnt/var/lib/libvirt

# 4. La particion EFI
mount -o umask=0077,shortname=winnt $ESP /mnt/efi

# 5. Desactivar CoW en el subvolumen de imagenes de VM
chattr +C /mnt/var/lib/libvirt

Qué hace cada opción:

  • noatime: no actualiza la fecha de último acceso de los ficheros cada vez que se leen. Reduce escrituras innecesarias y alarga la vida del SSD.
  • compress=zstd:1: comprime los datos con zstd en nivel 1. El nivel 1 es el más rápido; el ahorro de espacio compensa sin sacrificar rendimiento.
  • ssd: indica que el dispositivo es un SSD, activando optimizaciones específicas.
  • discard=async: envía las operaciones TRIM de forma asíncrona, sin bloquear las escrituras.
  • nosuid: ignora los bits SUID y SGID en los ficheros de ese subvolumen. Impide que un binario con SUID dentro del subvolumen pueda elevar privilegios.
  • nodev: ignora los ficheros de dispositivo. Evita que un atacante cree un dispositivo dentro del subvolumen para acceder al hardware.
  • noexec: impide ejecutar binarios desde ese subvolumen. Solo se aplica a /var/log, donde nada legítimo se ejecuta.
  • umask=0077: en la partición EFI, restringe los permisos por defecto a solo el propietario.
  • shortname=winnt: permite nombres de archivo 8.3 en la FAT32, por compatibilidad con el firmware.
  • chattr +C: activa el atributo NOCOW en el directorio de libvirt. Los archivos nuevos creados dentro heredan el atributo y dejan de someterse a copy-on-write.
⚠️
Por qué @containers lleva nodev pero no nosuid: las imágenes de contenedor incluyen binarios con SUID legítimos (por ejemplo ping, su o sudo dentro del contenedor). Si se monta con nosuid, esas imágenes fallan de forma difícil de diagnosticar. Se aísla la excepción en este subvolumen concreto en lugar de relajar /home entero.
⚠️
Sobre @libvirt: por qué NO se usa nodatacow como opción de montaje. En Btrfs, el control de copy-on-write no se hace con opciones de montaje sino con el atributo NOCOW a nivel de inodo. Poner nodatacow en el fstab afectaría a todo el sistema de ficheros, no solo a este subvolumen, porque en kernels modernos esa opción se aplica globalmente. La forma correcta es chattr +C sobre el directorio, que hace que los archivos nuevos hereden la propiedad sin afectar al resto del disco. Hay que aplicarlo antes de crear cualquier archivo dentro: si se hace después, los ya existentes conservan CoW.
💡
Por qué no hay noexec en /home ni en /var: rompería los scripts de Python y Bash que vayas a ejecutar. Es un antipatrón habitual aplicar noexec en /home sin pensar; aquí se evita deliberadamente.
💡
Por qué los puntos de montaje anidados se crean dentro del subvolumen contenedor: al montar un subvolumen sobre un directorio, todo el contenido previo de ese directorio queda oculto. Si se quiere montar @varlog en /mnt/var/log, el directorio log debe existir dentro de @var, no en @. Por eso se monta @var temporalmente, se crean dentro los subdirectorios necesarios y se desmonta antes del montaje definitivo.

2.99.7 Instalación del sistema base

Con la estructura de disco preparada y los subvolúmenes montados, el siguiente paso es descargar e instalar el sistema base sobre /mnt. Este paso se hace con pacstrap, que es un script que se encarga de inicializar un sistema Arch mínimo desde los repositorios oficiales.

7.1 Configurar los mirrors

Antes de instalar nada conviene actualizar la lista de mirrors. Por defecto, el live trae una lista enorme de servidores de todo el mundo; si se usa tal cual, pacstrap puede tardar mucho porque elige mirrors lentos. El comando reflector filtra por país, protocolo y velocidad, y deja la lista ordenada de más rápido a más lento:

bash
reflector --country Spain,France,Portugal --protocol https \
    --latest 20 --sort rate --save /etc/pacman.d/mirrorlist

Qué hace cada opción:

  • --country Spain,France,Portugal: limita la búsqueda a mirrors ubicados en esos países. Reduce la latencia y la probabilidad de caídas.
  • --protocol https: solo acepta mirrors que sirvan por HTTPS. Evita HTTP en claro, que permitiría manipulación de paquetes en tránsito.
  • --latest 20: se queda con los 20 mirrors más recientes en sincronizarse con el repositorio oficial. Los mirrors desactualizados pueden servir paquetes viejos o inconsistentes.
  • --sort rate: ordena los mirrors resultantes por velocidad de descarga medida, poniendo los más rápidos primero.
  • --save /etc/pacman.d/mirrorlist: escribe el resultado en el archivo de configuración de pacman, reemplazando la lista por defecto.

Verifica que no ha quedado ningún mirror por HTTP:

bash
grep "^Server" /etc/pacman.d/mirrorlist | grep -vc "^Server = https"

Qué hace cada parte:

  • grep "^Server": extrae solo las líneas que definen un servidor de la lista de mirrors.
  • grep -vc "^Server = https": cuenta (-c) las líneas que no coinciden (-v) con el patrón "empieza por Server = https". El resultado debe ser 0.

Si devuelve cualquier otro número, algún mirror quedó por HTTP y conviene corregir la lista antes de continuar.

7.2 Instalar el sistema base con pacstrap

pacstrap instala un conjunto de paquetes sobre el directorio indicado. Aquí se listan explícitamente los que formarán la base del sistema, sin añadir grupos completos:

bash
pacstrap -K /mnt \
    base linux-hardened linux linux-firmware amd-ucode \
    btrfs-progs cryptsetup \
    mkinitcpio sbctl systemd-ukify \
    efibootmgr \
    apparmor \
    nftables \
    zram-generator \
    iwd \
    sudo vim \
    man-db man-pages \
    python

Qué hace cada cosa:

  • pacstrap -K: instala paquetes en el directorio indicado (/mnt en este caso) y usa una base de datos de paquetes vacía en lugar de reutilizar la del live. El flag -K garantiza que se descargan las firmas actualizadas de todos los paquetes.

Qué instala cada paquete y por qué:

  • base: conjunto mínimo de paquetes que forman la base de un sistema Arch. Incluye bash, coreutils, systemd, pacman y otros imprescindibles.
  • linux-hardened: kernel con parches de endurecimiento. Es el kernel principal del sistema. Aplica mitigaciones adicionales, restricciones de memoria y controles de seguridad que no están en el kernel estándar.
  • linux: kernel estándar, como respaldo. Si alguna actualización deja linux-hardened sin arrancar, tienes una segunda entrada firmada esperando.
  • linux-firmware: metapaquete con el firmware de la mayoría de dispositivos comunes. Incluye el driver iwlwifi para el Wi-Fi Intel y el firmware de la GPU Radeon integrada. Sin él no hay Wi-Fi ni aceleración gráfica.
  • amd-ucode: microcódigo del procesador AMD. Corrige vulnerabilidades de canal lateral como TSA, Spectre o Meltdown que se aplican en tiempo de arranque.
  • btrfs-progs: herramientas para gestionar Btrfs (crear subvolúmenes, snapshots, revisar el sistema de ficheros).
  • cryptsetup: utilidad para gestionar volúmenes LUKS. Sin ella el sistema no puede descifrar el disco al arrancar.
  • mkinitcpio: genera la initramfs, el archivo intermedio que carga el kernel y monta la raíz.
  • sbctl: herramienta para gestionar Secure Boot con claves propias.
  • systemd-ukify: genera Unified Kernel Images firmables con Secure Boot.
  • apparmor: módulo de control de acceso obligatorio (MAC) del kernel. Confina procesos a un conjunto de permisos predefinidos.
  • nftables: framework moderno de filtrado de paquetes. Reemplaza a iptables.
  • zram-generator: genera dispositivos zram al arrancar. Da swap comprimida en RAM sin tocar disco.
  • iwd: cliente Wi-Fi ligero y moderno. Es la única forma de conectar red en este equipo al no tener Ethernet.
  • sudo: permite ejecutar comandos con privilegios elevados desde el usuario normal.
  • vim: editor de texto. Necesario para editar archivos de configuración en terminal.
  • man-db y man-pages: páginas de manual. Consulta offline de documentación.
  • python: intérprete de Python. Útil para scripts propios. No incluye pip ni base-devel de forma deliberada.
💡
Por qué no se instala base-devel ni python-pip: el compilador y las herramientas de compilación amplían innecesariamente la superficie de ataque del host. Cuando se necesiten para compilar algo puntual, se usará un contenedor desechable. El intérprete de Python sí se instala porque los scripts se ejecutan sin necesidad de compilar nada.

pacstrap descargará todos los paquetes, verificará sus firmas GPG contra el anillo de claves de Arch y los instalará. Al terminar, tendrás un sistema Arch mínimo con kernel, initramfs, firmwares y herramientas básicas.

7.3 Generar el fstab

El archivo /etc/fstab describe qué particiones y subvolúmenes se montan al arrancar. genfstab lo genera automáticamente a partir del estado actual de los montajes:

bash
genfstab -U /mnt >> /mnt/etc/fstab

Qué hace:

  • La línea de /efi lleva umask=0077.
  • No hay ninguna línea de swap en disco.
  • La línea de /var/lib/containers no lleva nosuid.
  • El atributo NOCOW está aplicado a /mnt/var/lib/libvirt con chattr +C (no aparece en el fstab porque Btrfs no lo gestiona como opción de montaje).

Antes de continuar, revisa el archivo generado y añade dos líneas manuales:

bash
vim /mnt/etc/fstab

Añade al final del archivo:

bash
tmpfs  /tmp      tmpfs  rw,nosuid,nodev,noexec,size=4G,mode=1777  0 0
tmpfs  /dev/shm  tmpfs  rw,nosuid,nodev,noexec,mode=1777          0 0

Qué hace cada opción:

  • /tmp en tmpfs: el directorio temporal vive en RAM, no en disco. Al apagar se borra. Además está limitado a 4 GB para evitar que un proceso consuma toda la memoria llenando /tmp.
  • /dev/shm en tmpfs: memoria compartida POSIX. También en RAM, con permisos restrictivos.
  • nosuid: ignora los bits SUID y SGID.
  • nodev: ignora los ficheros de dispositivo.
  • noexec: impide ejecutar binarios desde ese directorio. Ningún proceso legítimo necesita ejecutar desde /tmp ni desde /dev/shm.
  • mode=1777: permisos de directorio temporal estándar (lectura/escritura para todos, pero solo el propietario puede borrar sus archivos).

Antes de continuar, verifica también que /etc/fstab cumple lo siguiente:

  • La línea de /efi lleva umask=0077.
  • No hay ninguna línea de swap en disco.
  • La línea de /var/lib/libvirt lleva nodatacow.
  • La línea de /var/lib/containers no lleva nosuid.

7.4 Entrar en el sistema instalado

Con el sistema base instalado y el fstab generado, el siguiente paso es entrar en el nuevo sistema para terminar de configurarlo. arch-chroot cambia el directorio raíz a /mnt y ejecuta una shell dentro del nuevo entorno:

bash
arch-chroot /mnt

Qué hace:

  • arch-chroot: monta los sistemas de ficheros virtuales (/proc, /sys, /dev, etc.) dentro del directorio indicado y ejecuta una shell con ese directorio como raíz. A partir de este momento, cualquier comando se ejecuta como si el sistema instalado estuviera arrancado. El prompt cambia para reflejarlo.

Para salir del chroot cuando termines, basta con ejecutar exit. Volverás al entorno del live, donde /mnt sigue siendo el punto de montaje del sistema instalado.

2.99.8 Configuración base y zram

Dentro del chroot, el sistema está aislado del live y cualquier comando afecta al sistema instalado. Este bloque cubre la configuración mínima para que el sistema arranque correctamente: zona horaria, locale, hostname, swap comprimida en RAM, usuario y red.

8.1 Zona horaria y reloj

bash
ln -sf /usr/share/zoneinfo/Europe/Madrid /etc/localtime
hwclock --systohc

Qué hace cada uno:

  • ln -sf: crea un enlace simbólico. Fuerza (-f) la creación aunque exista uno previo, apuntando /etc/localtime al archivo de zona horaria correspondiente. A partir de ahí, el sistema sabe en qué zona horaria está.
  • hwclock --systohc: escribe la hora actual del sistema (system clock) en el reloj de hardware (hardware clock, la CMOS). Así el equipo arranca con la hora correcta sin depender de NTP durante los primeros segundos.

8.2 Locale

Los locales definen el idioma, el formato de fechas, números, monedas y codificación de caracteres. Hay que generar los que se vayan a usar y declarar cuál es el principal:

bash
sed -i 's/^#es_ES.UTF-8/es_ES.UTF-8/; s/^#en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
locale-gen

Qué hace cada uno:

  • sed -i: edita el archivo en sitio (sin crear copia). El patrón s/^#es_ES.UTF-8/es_ES.UTF-8/ descomenta la línea correspondiente a español de España. Se hace también con inglés de EE.UU. porque muchas herramientas y logs están en inglés y conviene tenerlo disponible.
  • locale-gen: lee /etc/locale.gen y genera los archivos de locale correspondientes en /usr/lib/locale. Sin este paso, el sistema no reconoce el locale aunque esté declarado.

Declara el locale principal y el mapa de teclado de la consola:

bash
echo "LANG=es_ES.UTF-8" > /etc/locale.conf
echo "KEYMAP=es" > /etc/vconsole.conf

Qué hace cada uno:

  • /etc/locale.conf: define las variables de entorno relacionadas con el locale. LANG es la principal; determina el idioma por defecto de la interfaz y los mensajes del sistema.
  • /etc/vconsole.conf: define el mapa de teclado de la consola virtual (la que se ve sin entorno gráfico). KEYMAP=es carga la distribución española de forma persistente al arrancar. Sin esto, el teclado sería inglés.

8.3 Hostname y hosts

bash
echo "matrix" > /etc/hostname
printf '127.0.0.1 localhost\n::1 localhost\n' > /etc/hosts

Qué hace cada uno:

  • /etc/hostname: contiene el nombre del equipo. Aparecerá en el prompt, en los logs y en la red.
  • /etc/hosts: resuelve nombres locales antes de consultar DNS. Las dos líneas que se escriben asocian localhost a las direcciones de loopback IPv4 (127.0.0.1) e IPv6 (::1). Es el mínimo imprescindible; añadir el hostname aquí no es necesario en un sistema moderno con systemd-resolved.

8.4 zram en lugar de swap en disco

Este equipo no lleva swap en disco: la swap va en RAM comprimida con zram. Eso elimina el riesgo de que datos sensibles acaben escritos sin cifrar y es coherente con la decisión de apagar al cerrar la tapa.

bash
cat > /etc/systemd/zram-generator.conf << 'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
swap-priority = 100
fs-type = swap
EOF

Qué hace cada línea:

  • [zram0]: sección que define el primer dispositivo zram. El generador crea uno por cada sección con este formato.
  • zram-size = ram / 2: tamaño del dispositivo zram, la mitad de la RAM total. Con 15 GB de RAM, serán unos 7,5 GB comprimidos, que con zstd equivalen a bastante más en la práctica.
  • compression-algorithm = zstd: algoritmo de compresión. zstd ofrece un buen equilibrio entre velocidad y ratio de compresión.
  • swap-priority = 100: prioridad alta. Si en algún momento hubiera otra swap (no es el caso), el kernel preferiría esta por ser más rápida.
  • fs-type = swap: el dispositivo se formatea como swap en lugar de como sistema de ficheros normal.

Ajusta también los parámetros del kernel para que la swap se comporte bien sobre zram:

bash
cat > /etc/sysctl.d/99-zram.conf << 'EOF'
vm.swappiness = 180
vm.watermark_boost_factor = 0
vm.watermark_scale_factor = 125
vm.page-cluster = 0
EOF

Qué hace cada parámetro:

  • vm.swappiness = 180: indica al kernel que tienda a usar swap con más agresividad. Los valores habituales son 10 o 60 para swap en disco. Con zram conviene justo lo contrario: swapear a RAM comprimida es rápido, así que interesa hacerlo más a menudo.
  • vm.watermark_boost_factor = 0: desactiva el boost del watermark de memoria, que no aporta nada con zram.
  • vm.watermark_scale_factor = 125: incrementa la distancia entre los watermarks de memoria libre. Da al kernel más margen para reaccionar antes de que la memoria se agote.
  • vm.page-cluster = 0: lee páginas de swap de una en una, no en clusters de 8. Con zram no tiene sentido leer varias páginas juntas, porque ya se leen de RAM y no hay coste de disco que amortizar.

8.5 Crear el usuario

bash
useradd -m -G wheel -s /bin/bash cambiame
passwd cambiame

Qué hace cada uno:

  • useradd: crea un usuario nuevo. Sustituye cambiame por el nombre que quieras.
  • -m: crea el directorio personal del usuario (/home/usuario). Sin este flag, el directorio no se crea y el usuario tendría problemas al iniciar sesión.
  • -G wheel: añade el usuario al grupo wheel. Ese grupo se usará después para conceder permisos de sudo.
  • -s /bin/bash: establece bash como shell del usuario.
  • passwd: pide y establece la contraseña del usuario.

Configura sudo para que el grupo wheel pueda elevar privilegios:

bash
EDITOR=vim visudo

Qué hace:

  • EDITOR=vim: fuerza a visudo a usar vim como editor.
  • visudo: abre /etc/sudoers en modo edición segura. Antes de guardar, valida la sintaxis; si hay un error, se niega a escribir. Editar /etc/sudoers directamente puede dejar el sistema sin sudo si se comete un error tipográfico.

Busca la línea y descoméntala (quita el # inicial):

bash
%wheel ALL=(ALL:ALL) ALL

Qué significa:

  • %wheel: aplica a todos los miembros del grupo wheel.
  • ALL=(ALL:ALL): el usuario puede ejecutar comandos como cualquier usuario y cualquier grupo.
  • ALL final: desde cualquier terminal, cualquier comando.
⚠️
No bloquees root todavía. El bloqueo de la cuenta root se hará más adelante, cuando se haya verificado que sudo funciona correctamente. Si bloqueas root ahora y sudo tiene algún problema, te quedarás fuera del sistema.

8.6 Red: iwd y systemd-networkd

Este equipo solo tiene Wi-Fi, así que iwd es la única forma de conectarse. Habilita también systemd-networkd para la gestión de interfaces y systemd-resolved para la resolución DNS:

bash
systemctl enable iwd systemd-networkd systemd-resolved apparmor

Qué hace cada uno:

  • systemctl enable iwd: activa iwd al arrancar. Será el cliente que gestione las conexiones Wi-Fi.
  • systemctl enable systemd-networkd: gestiona las interfaces de red (direcciones IP, rutas).
  • systemctl enable systemd-resolved: gestiona la resolución de nombres y ofrece caché DNS local.
  • systemctl enable apparmor: carga los perfiles de AppArmor al arrancar. En este punto no hay perfiles propios, pero dejamos el servicio activo para cuando los añadamos.

Configura iwd:

bash
mkdir -p /etc/iwd
cat > /etc/iwd/main.conf << 'EOF'
[General]
AddressRandomization=network
AddressRandomizationRange=full
EnableNetworkConfiguration=true

[Network]
EnableIPv6=true
NameResolvingService=systemd
EOF

Qué hace cada opción:

  • AddressRandomization=network: genera una MAC distinta para cada red Wi-Fi a la que se conecte el equipo. La MAC será estable dentro de una misma red (para no romper portales cautivos o reservas DHCP), pero distinta entre redes.
  • AddressRandomizationRange=full: usa el rango completo de direcciones MAC locales, maximizando la variabilidad.
  • EnableNetworkConfiguration=true: permite a iwd configurar la interfaz (dirección IP, rutas) por sí mismo, integrando DHCP y SLAAC.
  • EnableIPv6=true: habilita IPv6. No se desactiva IPv6 porque no hay motivo de seguridad para hacerlo y rompería compatibilidad con muchas redes modernas.
  • NameResolvingService=systemd: delega la resolución DNS a systemd-resolved, que ofrece caché y soporte de DoT.

Enlaza el archivo de resolución de systemd-resolved con el que consultan las aplicaciones:

bash
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

Qué hace:

  • ln -sf: crea un enlace simbólico que apunta /etc/resolv.conf al stub que genera systemd-resolved en tiempo de ejecución. Así las aplicaciones que consultan DNS a través de /etc/resolv.conf pasan por systemd-resolved sin necesidad de configurar nada más.
💡
Sobre la aleatorización de MAC: cambiar la MAC en cada red impide el seguimiento pasivo por parte de terceros que registran qué dispositivos se conectan a qué puntos de acceso. Mantenerla estable dentro de la misma red evita problemas con portales de hotel, aeropuertos o redes corporativas que asocian sesiones a una MAC concreta.

8.7 Resetear las MACs asociadas a redes

Con AddressRandomization=network configurado en iwd, el sistema genera una MAC distinta para cada red Wi-Fi, derivada de forma estable a partir del identificador de la red. Esto significa que, mientras iwd recuerde esa red, seguirá usando la misma MAC. Si quieres que iwd genere una MAC nueva para todas las redes a las que te has conectado, hay que borrar los perfiles guardados. Al reconectarte, iwd creará perfiles nuevos y, con ellos, nuevas direcciones MAC.

bash
sudo rm /var/lib/iwd/*.psk /var/lib/iwd/*.open
sudo systemctl restart iwd

Qué hace cada parte:

  • rm /var/lib/iwd/*.psk: borra los perfiles de redes WPA2/WPA3 guardados. La extensión .psk es la que usa iwd para almacenar la contraseña y los datos de la red.
  • rm /var/lib/iwd/*.open: borra los perfiles de redes abiertas (sin contraseña), que iwd guarda con extensión .open.
  • systemctl restart iwd: reinicia el servicio para que vuelva a leer la configuración desde cero y no conserve en memoria los perfiles antiguos.
⚠️
Esto borra también las contraseñas Wi-Fi guardadas. Al reconectarte a cada red, tendrás que volver a introducir la contraseña. Si solo quieres resetear una red concreta, borra únicamente su archivo. Por ejemplo, para olvidar solo la red llamada MiWiFi:
bash
sudo rm /var/lib/iwd/MiWiFi.psk
sudo systemctl restart iwd
El nombre del archivo es el SSID de la red seguido de la extensión correspondiente.
💡
Cuándo conviene hacer esto: si has estado conectado a muchas redes (cafeterías, aeropuertos, hoteles, oficinas de clientes) y quieres romper cualquier posible correlación entre ellas, borrar los perfiles fuerza la generación de nuevas MACs la próxima vez que te conectes a cada una. Es una operación puntual, no algo que haya que hacer cada día.

2.99.9 initramfs, línea de comandos y UKI firmables

Este es el bloque central para todo lo relacionado con Secure Boot. La idea es que los parámetros del kernel queden dentro del binario firmado, de forma que nadie pueda añadir opciones como lockdown=none o apparmor=0 desde el firmware sin invalidar la firma. Para conseguirlo se usan Unified Kernel Images (UKI), que empaquetan en un solo binario el kernel, la initramfs y la línea de comandos.

9.1 Configurar los hooks de mkinitcpio

La initramfs es el archivo intermedio que carga el kernel al arrancar y que se encarga de montar la raíz real. En este caso, como la raíz está cifrada, tiene que incluir el soporte de LUKS y del TPM antes de que el sistema pueda continuar.

bash
vim /etc/mkinitcpio.conf

Deja la línea HOOKS así:

bash
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck)

Qué hace cada hook:

  • base: incluye las herramientas básicas y el script principal de initramfs.
  • systemd: usa systemd como gestor del arranque dentro de la initramfs. Reemplaza al hook udev tradicional. Necesario para que funcione sd-encrypt.
  • autodetect: detecta automáticamente los módulos del hardware presente y los incluye en la initramfs. Reduce el tamaño del archivo generado.
  • microcode: añade el microcódigo de CPU que se haya instalado (amd-ucode en este caso) para que se cargue antes que el kernel.
  • modconf: carga la configuración de módulos definida en /etc/modprobe.d/.
  • kms: incluye los módulos de kernel de modosetting gráficos (KMS). Permite mostrar la pantalla de descifrado a resolución nativa y sin parpadeos.
  • keyboard: incluye los módulos de teclado para poder escribir la contraseña de LUKS. Debe ir antes de sd-encrypt.
  • sd-vconsole: aplica el mapa de teclado definido en /etc/vconsole.conf desde el momento del arranque. Sin él, el teclado español no se aplica hasta que arranca el sistema completo, lo cual es un problema si la contraseña de LUKS lleva caracteres como la ñ.
  • block: carga los módulos necesarios para acceder a los dispositivos de bloque (discos).
  • sd-encrypt: monta el volumen LUKS usando systemd-cryptsetup. Es lo que permite el desbloqueo por TPM. Reemplaza al hook encrypt tradicional.
  • filesystems: carga los módulos de sistemas de ficheros que no están compilados en el kernel.
  • fsck: comprueba la integridad de los sistemas de ficheros antes de montarlos.
💡
Por qué sd-vconsole y no keymap: si usas systemd como hook de arranque, el hook correcto para el mapa de teclado es sd-vconsole. El tradicional keymap solo funciona con el flujo base + udev.

9.2 Definir la línea de comandos del kernel

Los parámetros del kernel se escriben en archivos dentro de /etc/cmdline.d/. Todos los archivos .conf de ese directorio se concatenan en orden alfabético para formar la línea de comandos final. Separar por archivos temáticos facilita el mantenimiento.

Primero, obtén el UUID del volumen LUKS y guárdalo en una variable para no equivocarte al copiarlo:

bash
LUKS_UUID=$(blkid -s UUID -o value /dev/nvme0n1p2)
echo "UUID detectado: $LUKS_UUID"

Qué hace cada parte:

  • blkid -s UUID -o value: consulta la información del dispositivo indicado. -s UUID pide solo el campo del UUID; -o value devuelve únicamente el valor, sin el nombre del campo.
  • $(...): asigna el resultado del comando a la variable LUKS_UUID.
  • echo: muestra el valor para poder verificarlo visualmente.

Ahora crea los archivos de configuración:

bash
mkdir -p /etc/cmdline.d

cat > /etc/cmdline.d/00-root.conf << EOF
rd.luks.name=$LUKS_UUID=cryptroot
rd.luks.options=$LUKS_UUID=tpm2-device=auto,discard
root=/dev/mapper/cryptroot
rootflags=subvol=@
rw
EOF

Qué hace cada parámetro:

  • rd.luks.name=UUID=cryptroot: indica a la initramfs que abra el volumen LUKS con ese UUID y lo exponga como /dev/mapper/cryptroot.
  • rd.luks.options=UUID=tpm2-device=auto,discard: opciones específicas para este volumen. tpm2-device=auto permite el desbloqueo automático con TPM; discard permite enviar TRIM al SSD, importante para no perder rendimiento.
  • root=/dev/mapper/cryptroot: define el dispositivo raíz, que es el volumen LUKS ya descifrado.
  • rootflags=subvol=@: indica que la raíz está en el subvolumen @ de Btrfs.
  • rw: monta la raíz en modo lectura-escritura desde el principio.
bash
cat > /etc/cmdline.d/10-lsm.conf << 'EOF'
lsm=landlock,lockdown,yama,integrity,apparmor,bpf
lockdown=integrity
EOF

Qué hace cada parámetro:

  • lsm=...: define el orden en que se cargan los Linux Security Modules. Todos los listados deben estar disponibles; si falta alguno, el arranque puede caer al modo de emergencia.
  • lockdown=integrity: activa el modo integridad del lockdown del kernel. Impide que incluso root pueda modificar el kernel en caliente (cargar módulos no firmados, acceder a /dev/mem, usar kexec, etc.).
bash
cat > /etc/cmdline.d/20-hardening.conf << 'EOF'
init_on_alloc=1
init_on_free=1
slab_nomerge
page_alloc.shuffle=1
randomize_kstack_offset=on
vsyscall=none
debugfs=off
module.sig_enforce=1
EOF

Qué hace cada parámetro:

  • init_on_alloc=1: inicializa a cero la memoria asignada por el kernel. Evita fugas de datos residuales de procesos anteriores.
  • init_on_free=1: inicializa a cero la memoria liberada. Complementa al anterior y cierra la ventana de reutilización de memoria con datos sensibles.
  • slab_nomerge: impide que el kernel combine cachés de slab. Dificulta ataques de tipo heap grooming.
  • page_alloc.shuffle=1: aleatoriza el orden en que se asignan las páginas de memoria física. Dificulta ataques que dependen de la colocación predecible de memoria.
  • randomize_kstack_offset=on: aleatoriza la posición inicial del stack del kernel. Dificulta exploits de desbordamiento de pila en el kernel.
  • vsyscall=none: desactiva el mecanismo legacy vsyscall. Elimina una superficie conocida de exploits.
  • debugfs=off: desactiva el sistema de ficheros debugfs. No se necesita en un sistema en producción.
  • module.sig_enforce=1: solo permite cargar módulos firmados. Refuerza la política de lockdown.
bash
cat > /etc/cmdline.d/30-amd.conf << 'EOF'
amd_iommu=force_isolation
iommu.passthrough=0
iommu.strict=1
amd_pstate=active
EOF

Qué hace cada parámetro:

  • amd_iommu=force_isolation: en AMD, el IOMMU ya está activo por defecto, pero este parámetro fuerza el aislamiento por dispositivo en lugar de dominios compartidos. Cierra el DMA entre periféricos. Si tras arrancar algún dispositivo falla, se puede quitar esta línea y regenerar la UKI.
  • iommu.passthrough=0: fuerza a que todas las operaciones de DMA pasen por la IOMMU, sin excepciones.
  • iommu.strict=1: activa el modo estricto de invalidación de TLB. Más seguro que el modo diferido, a costa de un pequeño coste de rendimiento.
  • amd_pstate=active: activa el driver de gestión de frecuencia de CPU en modo activo. Mejora la gestión de energía y batería. No es seguridad, es eficiencia.
bash
cat > /etc/cmdline.d/40-quiet.conf << 'EOF'
quiet
loglevel=3
EOF

Qué hace cada parámetro:

  • quiet: reduce el ruido durante el arranque, mostrando solo los mensajes importantes.
  • loglevel=3: establece el nivel mínimo de mensajes que se muestran en consola. El nivel 3 incluye errores, pero no advertencias ni información.
⚠️
Sin mitigations=auto,nosmt. El SMT (Simultaneous Multithreading) del procesador está activo y así se queda. Desactivarlo implicaría perder la mitad del rendimiento, y para ataques de canal lateral entre hilos hermanos se necesitan condiciones muy específicas. Está documentado como excepción aceptada.
💡
Sin resume=. No hay hibernación porque no hay swap en disco. Es una decisión coherente con el hecho de que la tapa apague el equipo.

9.3 Crear los presets de UKI

Cada kernel instalado tiene un archivo de preset en /etc/mkinitcpio.d/ que define cómo se genera su UKI. En este caso vamos a tener dos kernels (linux-hardened y linux), cada uno con una versión default y otra fallback.

bash
cat > /etc/mkinitcpio.d/linux-hardened.preset << 'EOF'
ALL_kver=A

PRESETS=(A B)

default_image=A
default_uki=A
default_options=A

fallback_image=A
fallback_uki=A
fallback_options=A
EOF

cat > /etc/mkinitcpio.d/linux.preset << 'EOF'
ALL_kver=A

PRESETS=(A B)

default_image=A
default_uki=A
default_options=A

fallback_image=A
fallback_uki=A
fallback_options=A
EOF

Qué hace cada opción:

  • ALL_kver: ruta del kernel para el que se generan las UKI.
  • PRESETS: variantes que se generan. default es la UKI normal, con todos los módulos que necesita el hardware actual. fallback es una versión que no usa el hook autodetect, por lo que incluye muchos más módulos y sirve como rescate si el sistema cambia de hardware o algo falla.
  • default_image y fallback_image: ruta de la initramfs temporal que se genera antes de empaquetarla dentro de la UKI. Se deja en /tmp porque desaparece al reiniciar y no ocupa espacio permanentemente.
  • default_uki y fallback_uki: rutas donde se escriben las UKI finales. Van dentro de la partición EFI, en /efi/EFI/Linux/.
  • default_options y fallback_options: opciones de ukify, la herramienta que empaqueta la UKI. --splash añade una imagen de arranque; -S autodetect desactiva el hook autodetect para la versión fallback.
⚠️
Las líneas default_image y fallback_image no son opcionales. Aunque las UKI se generan con default_uki y fallback_uki, la versión de mkinitcpio que acompaña a las ISOs recientes de Arch sigue exigiendo que se declare explícitamente una ruta para la initramfs temporal. Si falta, mkinitcpio -P no da error pero imprime un aviso por cada preset (WARNING: No image or UKI specified. Skipping image) y termina sin haber generado ninguna UKI. El sistema no arrancará porque el firmware no encontrará nada que ejecutar. Es un fallo silencioso: si no se revisa la salida de mkinitcpio -P, no se detecta hasta el reinicio.

Ninguna línea _image= apunta a /boot: eso significa que las initramfs temporales no se conservan, solo se usan como paso intermedio. /boot es un directorio dentro del subvolumen cifrado, así que el vmlinuz vive cifrado y lo único en claro es la UKI de la ESP, que va firmada.

Genera las UKI:

bash
mkdir -p /efi/EFI/Linux /efi/EFI/BOOT
mkinitcpio -P

Qué hace:

  • mkdir -p: crea los directorios donde se escribirán las UKI, si no existen.
  • mkinitcpio -P: ejecuta la generación de todas las UKI definidas en los presets. La P mayúscula indica "todos los presets".

La salida debe incluir el mensaje Unified kernel image generation successful cuatro veces: dos kernels por dos variantes cada uno. Es normal ver avisos sobre firmware opcional que no se encuentra (Possible missing firmware); hacen referencia a módulos de GPU o Wi-Fi que no tienes y no impiden el arranque. Lo que no debe aparecer es el aviso WARNING: No image or UKI specified.

9.4 Crear entradas de arranque y respaldo

Ahora hay que decirle al firmware que arranque la UKI. Se crean dos entradas: una para linux-hardened y otra para el kernel de rescate.

bash
efibootmgr --create --disk /dev/nvme0n1 --part 1 \
    --label "Arch Linux (hardened)" \
    --loader '\EFI\Linux\arch-linux-hardened.efi' --unicode

efibootmgr --create --disk /dev/nvme0n1 --part 1 \
    --label "Arch Linux (rescate)" \
    --loader '\EFI\Linux\arch-linux.efi' --unicode

efibootmgr -v

Qué hace cada opción:

  • --create: crea una nueva entrada de arranque.
  • --disk y --part: disco y partición donde está la UKI.
  • --label: nombre que aparecerá en el menú de arranque del firmware.
  • --loader: ruta de la UKI dentro de la partición EFI. Las barras invertidas son las que usa el firmware UEFI.
  • --unicode: codifica correctamente los caracteres no ASCII del label.
  • -v: muestra todas las entradas existentes en modo detallado, para verificar que se han creado bien.

Red de seguridad específica de ASUS. Algunos firmwares de Zenbook ignoran o descartan las entradas NVRAM personalizadas tras una actualización o un corte de corriente. Copia la UKI también a la ruta de arranque por defecto, que el firmware siempre encuentra:

bash
cp /efi/EFI/Linux/arch-linux-hardened.efi /efi/EFI/BOOT/BOOTX64.EFI

Qué hace:

  • cp: copia la UKI de hardened al archivo BOOTX64.EFI. Ese nombre concreto es el que el firmware UEFI busca por defecto si no encuentra otra entrada configurada. Es la vía de rescate si la NVRAM se borra.

Para que esa copia se mantenga actualizada y firmada, se añade un hook de pacman que la regenera cada vez que se actualiza el kernel o systemd:

bash
mkdir -p /etc/pacman.d/hooks
cat > /etc/pacman.d/hooks/95-uki-fallback.hook << 'EOF'
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = linux-hardened
Target = systemd
Target = mkinitcpio

[Action]
Description = Copiando y firmando la UKI de respaldo en EFI/BOOT...
When = PostTransaction
Exec = /bin/sh -c '/usr/bin/cp /efi/EFI/Linux/arch-linux-hardened.efi /efi/EFI/BOOT/BOOTX64.EFI && /usr/bin/sbctl sign /efi/EFI/BOOT/BOOTX64.EFI'
Depends = sbctl
EOF

Qué hace cada bloque:

  • [Trigger]: define cuándo se ejecuta el hook. Aquí, tras instalar o actualizar linux-hardened, systemd o mkinitcpio.
  • [Action]: define qué hace el hook. Copia la UKI de hardened al respaldo y le aplica la firma de Secure Boot con sbctl.
  • Depends = sbctl: garantiza que sbctl esté instalado antes de ejecutar el hook.

9.5 Comprobación antes de salir del chroot

Verifica que el UUID del archivo de línea de comandos coincide exactamente con el del volumen LUKS. Un dígito mal aquí es la causa del 90 % de los "no arranca".

bash
cat /etc/cmdline.d/00-root.conf
blkid -s UUID -o value /dev/nvme0n1p2

Qué hace cada uno:

  • cat /etc/cmdline.d/00-root.conf: muestra el contenido del archivo, donde está el UUID que se usará al arrancar.
  • blkid: consulta el UUID real del volumen LUKS. Ambos valores deben ser idénticos carácter a carácter.

Comprueba también que las UKI existen en la partición EFI:

bash
ls -la /efi/EFI/Linux/ /efi/EFI/BOOT/

Qué hace:

  • ls -la: lista con detalles (permisos, tamaño, fecha) los archivos en ambos directorios. Deben aparecer las cuatro UKI en /efi/EFI/Linux/ y el BOOTX64.EFI en /efi/EFI/BOOT/.

Si todo está correcto, sal del chroot y reinicia:

bash
exit
umount -R /mnt
reboot

Qué hace cada uno:

  • exit: sale del chroot y vuelve al entorno del live.
  • umount -R /mnt: desmonta recursivamente todos los sistemas de ficheros montados bajo /mnt, en orden inverso al de montaje. Es importante hacerlo antes de reiniciar para que los datos se sincronicen a disco correctamente.
  • reboot: reinicia el equipo. Quita el USB antes de que arranque, o el firmware podría volver a cargar el live en lugar del sistema instalado.

2.99.10 Primer arranque y verificación inicial

El sistema ya arranca. Ahora toca iniciar sesión y comprobar que todo lo que configuramos en el chroot se ha aplicado correctamente. Esta verificación es importante antes de seguir con el endurecimiento, porque si algo falla en este punto es mucho más fácil de diagnosticar ahora que después de añadir más capas.

10.1 Iniciar sesión

En el prompt de login introduce el nombre del usuario que creaste (en la guía usamos cambiame como ejemplo) y su contraseña. No uses root; esa cuenta está bloqueada y no debe usarse para el trabajo diario.

Si el login no acepta la contraseña, revisa que el teclado esté en español. En la consola, el mapa de teclado se aplica desde /etc/vconsole.conf, que ya configuramos. Si aun así falla, puede que la contraseña tenga caracteres que no se están escribiendo bien. En ese caso, arranca con el USB, entra en el chroot y restablece la contraseña con passwd cambiame.

10.2 Conectar el Wi-Fi

iwd está habilitado como servicio, pero no conserva los perfiles de red del live. Hay que conectarse de nuevo usando iwctl:

bash
iwctl
[iwd]# station wlan0 connect NOMBRE_DE_TU_RED
[iwd]# exit

Qué hace:

  • iwctl: abre el cliente interactivo de iwd.
  • station wlan0 connect: inicia la conexión a la red indicada. Pedirá la contraseña si es WPA2 o WPA3.
  • exit: sale del cliente interactivo.

Comprueba que hay conexión:

bash
ping -c3 archlinux.org

Qué hace:

  • ping: envía paquetes ICMP al destino. Con -c3 envía tres y termina. Si hay respuesta, la red funciona.

10.3 Verificar la línea de comandos del kernel

Los parámetros que escribimos en /etc/cmdline.d/ deben estar aplicados. Compruébalo con estos comandos:

bash
cat /sys/kernel/security/lockdown

Qué hace:

  • Muestra el estado del lockdown del kernel. Debe aparecer [integrity], lo que indica que está activo en modo integridad. Si aparece [none], la línea lockdown=integrity no se ha aplicado y habría que revisar el archivo 10-lsm.conf.
bash
aa-enabled

Qué hace:

  • Comprueba si AppArmor está activo. Debe responder Yes. Si responde No, el servicio no ha arrancado o no está habilitado.
bash
zramctl

Qué hace:

  • Muestra los dispositivos zram activos. Debe aparecer uno llamado /dev/zram0 con un tamaño aproximado de la mitad de la RAM (unos 7,5 GB si tienes 15 GB). Si no aparece nada, el generador de zram no ha arrancado o la configuración tiene un error.
bash
cat /sys/devices/system/cpu/vulnerabilities/tsa

Qué hace:

  • Muestra el estado de mitigación de TSA. Si dice Mitigation: Clear CPU buffers, el microcódigo se ha cargado correctamente y la vulnerabilidad está mitigada. Si dice Vulnerable: No microcode, hay que revisar que amd-ucode esté instalado y que el hook microcode esté presente en la initramfs.

10.4 Revisar el estado general

Comprueba que no haya servicios fallidos y que el journal no tenga errores graves:

bash
systemctl --failed

Qué hace:

  • Lista las unidades de systemd que han fallado. Si aparece alguna, investígala con systemctl status nombre.service. En un sistema recién instalado no debería haber ninguna.
bash
journalctl -p err -b

Qué hace:

  • Muestra los mensajes del journal de la sesión de arranque actual filtrados por prioridad de error. Es normal ver algún error puntual de hardware sin driver, pero no debería haber errores repetidos ni fallos graves de servicios.

10.5 Actualizar el sistema

Aunque acabamos de instalar desde los repositorios, conviene actualizar por si han entrado cambios desde entonces:

bash
sudo pacman -Syu

Qué hace:

  • sudo: ejecuta el comando como root.
  • pacman -Syu: sincroniza la base de datos de paquetes (-Sy) y actualiza el sistema completo (-u).

10.6 Sobre el TPM

En este punto el sistema pide la contraseña de LUKS manualmente cada vez que arranca. Como todavía no hemos sellado ninguna clave dentro de él, ese intento fallará de forma silenciosa y el sistema te pedirá la contraseña manualmente. Este comportamiento es correcto y esperado.

La configuración definitiva del TPM se hará en el siguiente bloque, cuando ya tengamos Secure Boot activado y podamos sellarlo correctamente.

💡
No te preocupes si el arranque tarda un poco en este primer inicio. systemd está generando por primera vez algunos archivos de estado y cachés. Los siguientes arranques serán notablemente más rápidos.

2.99.11 Secure Boot con claves propias

En este bloque vamos a activar Secure Boot usando nuestras propias claves en lugar de las de fábrica de Microsoft. La ventaja es que solo arrancarán los binarios que nosotros hayamos firmado. La desventaja es que cualquier actualización de firmware o cambio de configuración de la BIOS puede invalidar las firmas y dejarnos fuera, así que hay que hacerlo con cuidado.

11.1 Verificar el estado inicial

Antes de tocar nada, comprueba que el firmware está en modo de configuración (Setup Mode). Esto significa que no hay ninguna clave de plataforma activa y podemos instalar las nuestras:

bash
sudo pacman -S sbctl
sudo sbctl status

Qué hace:

  • sbctl status: muestra el estado actual del firmware respecto a Secure Boot. Debe indicar Setup Mode: Enabled y Secure Boot: Disabled.

Si Setup Mode aparece como Disabled, significa que el firmware todavía tiene las claves de fábrica y no permite escribir. En ese caso hay que entrar en la BIOS (ESC), ir a Security → Secure Boot Control [Enabled] → Key Management y seleccionar Clear Secure Boot Keys. Reinicia y vuelve a comprobar el estado.

⚠️
Si no encuentras la opción de Key Management, es posible que necesites tener configurada la contraseña de administrador de la BIOS para que aparezca. Si es así, ponla, guarda los cambios, reinicia y vuelve a entrar. Recuerda que en este modelo ASUS el menú puede tardar en aparecer hasta que la contraseña está establecida.

11.2 Crear las claves propias

Ahora generamos las claves criptográficas que usaremos para firmar todo lo que deba arrancar. sbctl crea tres pares de claves (PK, KEK y db) en /var/lib/sbctl:

bash
sudo sbctl create-keys

Qué hace:

  • sbctl create-keys: genera un juego completo de claves de Secure Boot. La PK (Platform Key) es la clave raíz del sistema. La KEK (Key Exchange Key) autoriza cambios en la base de datos de firmas. La db (Signature Database) contiene las claves con las que se firman los binarios.
⚠️
Haz copia de seguridad de /var/lib/sbctl. Si pierdes estas claves, no podrás firmar nuevos binarios y, si el firmware está en modo usuario (User Mode), no podrás arrancar nada que no esté ya firmado. La copia se hará formalmente en el bloque de emergencia, pero conviene tenerla presente desde ahora.

11.3 Enrolar las claves en el firmware

Con las claves creadas, hay que decírselo al firmware. sbctl se encarga de escribir las variables EFI correspondientes:

bash
sudo sbctl enroll-keys --microsoft

Qué hace:

  • enroll-keys: escribe las claves públicas (PK, KEK y db) en las variables EFI del firmware.
  • --microsoft: añade también los certificados de Microsoft a la base de datos. Sin esta opción, algunos componentes del firmware que están firmados por Microsoft (como las opciones de arranque por red) podrían dejar de funcionar. En un portátil doméstico no es crítico, pero ayuda a evitar sorpresas.

Tras ejecutarlo, reinicia la BIOS para asegurarte de que las claves se han escrito correctamente. Puedes comprobarlo de nuevo con sbctl status antes de continuar.

11.4 Firmar las UKI

Ahora hay que firmar cada una de las Unified Kernel Images que generamos en el bloque anterior. La opción -s es crítica: le dice a sbctl que guarde el archivo en su base de datos y lo vuelva a firmar automáticamente cada vez que se regenere:

bash
sudo sbctl sign -s /efi/EFI/Linux/arch-linux-hardened.efi
sudo sbctl sign -s /efi/EFI/Linux/arch-linux-hardened-fallback.efi
sudo sbctl sign -s /efi/EFI/Linux/arch-linux.efi
sudo sbctl sign -s /efi/EFI/Linux/arch-linux-fallback.efi
sudo sbctl sign -s /efi/EFI/BOOT/BOOTX64.EFI

Qué hace cada parte:

  • sign: firma el archivo indicado con la clave privada creada antes.
  • -s: añade el archivo a la base de datos de sbctl. Cada vez que se regenere (por ejemplo, tras una actualización de kernel), el hook de pacman lo volverá a firmar automáticamente. Sin -s, la próxima actualización dejaría la UKI sin firmar y el sistema no arrancaría.

Verifica que todos los archivos están firmados correctamente:

bash
sudo sbctl verify

Qué hace:

  • verify: comprueba que cada archivo firmado tiene una firma válida y que el firmware está configurado para confiar en ella. Debe mostrar todos los archivos como Signed.

11.5 Activar Secure Boot en la BIOS

Este paso se hace desde la BIOS, no desde Linux:

  1. Reinicia y pulsa ESC para entrar en la BIOS.
  2. Ve a Security → Secure Boot.
  3. Cambia Secure Boot Control de Disabled a Enabled.
  4. Guarda los cambios (F10) y reinicia.

Ahora mismo deberías tener la contraseña de administrador puesta. Si te la pide para cambiar esta opción, introdúcela.

11.6 Verificar que Secure Boot está activo

Una vez dentro de Arch Linux, comprueba el estado:

bash
sudo sbctl status

Qué hace:

  • Muestra el estado actual. Debe indicar Secure Boot: Enabled (User Mode). Esto significa que Secure Boot está activo y que el firmware está usando nuestras claves, no las de fábrica.
bash
bootctl status

Qué hace:

  • bootctl status: consulta el estado del arranque según systemd-boot. Debe mostrar Secure Boot: enabled.
🚨
Si tras activar Secure Boot el equipo no arranca, apágalo y vuelve a entrar en la BIOS. Desactiva Secure Boot, arranca de nuevo con el USB de Arch y revisa que todas las UKI estén firmadas con sbctl verify. Es muy probable que alguna se haya quedado sin firmar por no haber usado la opción -s en el paso anterior.

2.99.12 TPM, PIN y copias de emergencia

Con Secure Boot activo, ya podemos sellar el TPM. El TPM guardará una copia de la clave de LUKS, pero solo la liberará si el estado del arranque coincide exactamente con el que tenía en el momento del sellado. En este caso, se sella a PCR 7, que mide el estado de Secure Boot. Si alguien altera la cadena de arranque, el TPM no liberará la clave y el sistema pedirá la contraseña manual.

Pero antes de sellar nada, hay que preparar la salida de emergencia. El fTPM de AMD es firmware, no un chip aparte. Un Clear CMOS o una actualización de AGESA pueden resetearlo y perder el sellado. Cuando eso ocurra, necesitarás la contraseña de recuperación para entrar y volver a sellar. Si no la tienes, el disco es ilegible.

12.1 Copias de seguridad primero

Antes de tocar el TPM, haz dos copias: una de la cabecera LUKS y otra de la clave de recuperación.

La cabecera LUKS contiene la información necesaria para descifrar el disco. Si se corrompe (por un fallo del disco, un apagado brusco durante una operación de escritura, o un ataque que intente destruirla), el disco se vuelve inaccesible aunque tengas la contraseña. Guardar una copia en un USB externo te permite restaurarla y recuperar el acceso.

bash
sudo cryptsetup luksHeaderBackup /dev/nvme0n1p2 \
    --header-backup-file /root/luks-header-$(date +%F).img

Qué hace cada parte:

  • cryptsetup luksHeaderBackup: hace una copia de la cabecera del volumen LUKS, incluyendo los slots de claves y los metadatos de cifrado.
  • --header-backup-file: ruta donde se guarda la copia. El $(date +%F) añade la fecha actual en formato año-mes-día, para que no se sobrescriba si se hace más de una vez.

La segunda copia es la clave de recuperación. Es una clave aleatoria larga que systemd-cryptenroll puede generar como método alternativo de desbloqueo. Sirve para entrar cuando el TPM falle.

bash
sudo systemd-cryptenroll --recovery-key /dev/nvme0n1p2

Qué hace:

  • --recovery-key: genera una clave de recuperación aleatoria y la añade como un nuevo slot de LUKS. La clave se muestra una sola vez en pantalla y no se puede recuperar después. Cópiala en papel, literalmente, con mayúsculas, minúsculas y guiones exactos.
🚨
Apunta la clave de recuperación en papel. No la guardes en el propio portátil, ni en una nota del móvil, ni en un gestor de contraseñas que esté dentro del disco cifrado. Si el disco es lo que intentas recuperar, no puedes depender de algo que esté dentro de él. Papel, y guardado en un sitio físico distinto del portátil.

12.2 Sellar el TPM

Ahora sí, se sella la clave de LUKS dentro del TPM. A partir de este momento, el sistema podrá desbloquear el disco automáticamente si el estado del arranque es el esperado.

bash
sudo systemd-cryptenroll /dev/nvme0n1p2 \
    --tpm2-device=auto \
    --tpm2-pcrs=7 \
    --tpm2-with-pin=yes

Qué hace cada opción:

  • --tpm2-device=auto: usa el primer TPM disponible. Como solo tienes uno (el fTPM de AMD), no hace falta especificar la ruta.
  • --tpm2-pcrs=7: sella la clave a PCR 7, que mide el estado de Secure Boot. Si Secure Boot se desactiva o se modifican las claves, PCR 7 cambia y el TPM no liberará la clave.
  • --tpm2-with-pin=yes: exige un PIN además del estado del arranque. Es la diferencia entre "el sistema arranca solo" y "el sistema arranca cuando yo se lo autorizo". Sin PIN, cualquier persona que robe el portátil apagado puede arrancarlo y montar el disco sin saber nada. Con PIN, necesita el PIN además del equipo.

El PIN se pide dos veces al configurarlo, para evitar errores tipográficos. Elige algo que puedas recordar y teclear rápido en la pantalla de arranque, porque lo vas a escribir en cada encendido. Un PIN de 6 a 8 dígitos numéricos es un buen equilibrio entre comodidad y seguridad.

Verifica que el sellado se ha creado correctamente:

bash
sudo cryptsetup luksDump /dev/nvme0n1p2 | grep -E "systemd-tpm2|systemd-recovery|Tokens"

Qué hace:

  • luksDump: muestra la información de la cabecera LUKS, incluyendo todos los slots de claves activos.
  • grep: filtra por las líneas relevantes. Debe aparecer una entrada para systemd-tpm2 (el sellado) y otra para systemd-recovery (la clave de recuperación).

Reinicia para comprobar que todo funciona:

bash
sudo reboot

Ahora, en lugar de pedirte la contraseña completa de LUKS, el sistema te pedirá el PIN del TPM. Introdúcelo y el disco se descifrará automáticamente. Si pide la contraseña completa en lugar del PIN, revisa que la línea rd.luks.options de /etc/cmdline.d/00-root.conf sigue ahí con tpm2-device=auto.

⚠️
Solo con PCR 7, cualquier UKI firmada con tu clave abre el disco. Si alguien roba tu clave privada de firma, puede firmar una UKI que haga lo que quiera y PCR 7 seguirá siendo válido. Por eso son obligatorios el PIN y guardar /var/lib/sbctl fuera del equipo. Si te roban la clave de firma, PCR 7 deja de protegerte.

12.3 Crear el USB de emergencia

Ahora toca preparar el USB que te salvará cuando algo vaya mal. Este USB se guarda fuera del portátil, en un sitio físico distinto. Si lo llevas siempre contigo en la mochila, un robo del portátil y la mochila se lleva las dos cosas, y no sirve de nada.

Cifra el USB con LUKS para que si se pierde no quede nada expuesto:

bash
sudo cryptsetup luksFormat /dev/sdX1
sudo cryptsetup open /dev/sdX1 rescate
sudo mkfs.ext4 /dev/mapper/rescate
sudo mkdir -p /mnt/rescate
sudo mount /dev/mapper/rescate /mnt/rescate

Qué hace cada uno:

  • luksFormat: formatea la partición del USB con LUKS2. Sustituye /dev/sdX1 por el nombre real del USB, que puedes ver con lsblk.
  • cryptsetup open: abre el volumen cifrado para poder escribir dentro.
  • mkfs.ext4: formatea el volumen descifrado con ext4.
  • mount: monta el volumen en /mnt/rescate.

Copia los archivos críticos dentro del USB:

bash
sudo cp /root/luks-header-*.img /mnt/rescate/
sudo cp -a /var/lib/sbctl /mnt/rescate/sbctl-keys/
sudo umount /mnt/rescate
sudo cryptsetup close rescate
sudo shred -u /root/luks-header-*.img
⚠️
Si el primer comando falla con un error similar a cp: no se puede efectuar 'stat' sobre '/root/luks-header-*.img': No existe el fichero o el directorio, significa que la copia de la cabecera no se generó correctamente antes, o se guardó en otra ruta. En ese caso, genera la copia directamente en el USB. Asegúrate de que el volumen rescate sigue abierto y montado en /mnt/rescate y ejecuta:
bash
sudo cryptsetup luksHeaderBackup /dev/nvme0n1p2 \
    --header-backup-file /mnt/rescate/luks-header-$(date +%F).img

Una vez generada la cabecera directamente en el USB, continúa con los comandos habituales para copiar las claves de sbctl (sudo cp -a /var/lib/sbctl /mnt/rescate/sbctl-keys/) y luego desmonta y cierra el volumen con umount y cryptsetup close.

Qué hace cada uno:

  • cp /root/luks-header-*.img: copia la cabecera LUKS al USB. El * recoge todas las copias con fecha que hayas hecho.
  • cp -a /var/lib/sbctl: copia el directorio completo con las claves de Secure Boot. La opción -a preserva permisos y atributos.
  • umount y cryptsetup close: cierran el volumen y el contenedor cifrado de forma limpia.
  • shred -u: borra de forma segura la copia de la cabecera que quedó en /root. Sin -u, shred sobrescribe el archivo pero no lo elimina; con -u, lo borra después de sobrescribirlo.

El USB debe contener, como mínimo:

  • La cabecera LUKS.
  • La clave de recuperación LUKS, en papel aparte, no dentro del USB.
  • El directorio /var/lib/sbctl completo, con las claves de firma.
  • Una copia de la documentación de esta guía, por si la necesitas sin conexión.
  • La ISO de Arch verificada, por si necesitas arrancar desde cero.
💡
Prueba el USB antes de guardarlo. Ábrelo con la contraseña que le hayas puesto y comprueba que los archivos están dentro. Un USB de emergencia que no funciona es peor que no tenerlo, porque te da una falsa sensación de seguridad.

2.99.13 Base segura del sistema (Fase 1)

Con el sistema instalado, cifrado y arrancando con Secure Boot, es el momento de empezar a reducir la superficie de ataque. Esta fase se centra en tres cosas: asegurar la cadena de suministro de paquetes, auditar los binarios con privilegios y limpiar cuentas y servicios innecesarios.

13.1 Cadena de suministro

La primera línea de defensa es asegurarse de que los paquetes que se instalan vienen de donde dicen venir y no han sido manipulados. Arch Linux firma todos los paquetes de los repositorios oficiales, pero la configuración de pacman puede relajarse sin querer.

bash
sudo vim /etc/pacman.conf

Deja el bloque de opciones así:

bash
[options]
CheckSpace
VerbosePkgLists
ParallelDownloads = 5
SigLevel           = Required DatabaseOptional
LocalFileSigLevel  = Optional

Qué hace cada línea:

  • CheckSpace: verifica que hay espacio suficiente en disco antes de empezar una instalación o actualización. Evita que el sistema se quede a medias por falta de espacio.
  • VerbosePkgLists: muestra más información al listar paquetes, útil para diagnosticar conflictos.
  • ParallelDownloads = 5: descarga hasta cinco paquetes en paralelo. Acelera las actualizaciones sin saturar la red.
  • SigLevel = Required DatabaseOptional: exige que todos los paquetes estén firmados. DatabaseOptional permite que la base de datos de paquetes no esté firmada, pero los paquetes en sí sí deben estarlo.
  • LocalFileSigLevel = Optional: permite instalar paquetes locales (por ejemplo, desde un archivo .pkg.tar.zst) sin firma. Útil para paquetes propios o para compilaciones puntuales.

Comprueba que no haya ningún repositorio sospechoso ni ninguna directiva SigLevel = Never:

bash
sudo grep -rn "SigLevel" /etc/pacman.conf /etc/pacman.d/ | grep -v "^.*#"
grep "^\[" /etc/pacman.conf

Qué hace cada uno:

  • grep -rn "SigLevel": busca la palabra en todos los archivos de configuración de pacman. -r recorre directorios, -n muestra número de línea. Se excluyen las líneas comentadas con grep -v "^.*#".
  • grep "^\[": muestra solo las líneas que definen repositorios. Deben aparecer únicamente [options], [core] y [extra]. Si aparece cualquier otro repositorio (por ejemplo [blackarch]), conviene revisar por qué está ahí.
⚠️
Nada de repositorios externos en el host. El repositorio de BlackArch, por ejemplo, añade miles de herramientas ofensivas que amplían la superficie de ataque de forma innecesaria. Esas herramientas se ejecutan en la máquina virtual dedicada, no en el sistema principal.

13.2 Auditoría de binarios SUID y SGID

Los binarios con el bit SUID o SGID activo se ejecutan con los privilegios del propietario o del grupo, respectivamente. Son una vía clásica de escalada local de privilegios si tienen un fallo. Conviene saber cuáles hay y quitar los que no se usen.

bash
sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -printf "%M %u %g %p\n" 2>/dev/null | sort -k4

Qué hace cada parte:

  • find / -xdev: busca en todo el sistema de ficheros raíz, sin cruzar a otros sistemas montados como /home o /efi. Eso evita ruido y posibles errores en particiones externas.
  • \( -perm -4000 -o -perm -2000 \): encuentra archivos que tengan activado el bit SUID (4000) o el SGID (2000).
  • -type f: solo archivos regulares, no directorios ni dispositivos.
  • -printf "%M %u %g %p\n": formato de salida personalizado: permisos, usuario propietario, grupo propietario y ruta completa.
  • 2>/dev/null: descarta los mensajes de error de permisos denegados, que son numerosos al recorrer todo el sistema.
  • sort -k4: ordena por la cuarta columna, que es la ruta, para ver la lista organizada.

Guarda el resultado como referencia para comparar en el futuro:

bash
sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | sort | sudo tee /root/baseline-suid.txt

Qué hace:

  • tee: escribe el resultado tanto en pantalla como en el archivo indicado. Así ves la lista y la guardas a la vez.

En una instalación mínima como esta deberías ver alrededor de una docena de binarios. Muchos de ellos son necesarios para el funcionamiento normal del sistema. Sin embargo, algunos solo se usan en casos muy concretos y pueden desactivarse:

bash
sudo chmod u-s /usr/bin/chfn /usr/bin/chsh

Qué hace:

  • chmod u-s: quita el bit SUID al archivo. chfn y chsh permiten cambiar la información del usuario y la shell por defecto, respectivamente. En un sistema de uso personal no suelen necesitarse.

Para que estos cambios no se reviertan en la próxima actualización de los paquetes que los contienen, se añade un hook de pacman:

bash
sudo mkdir -p /etc/pacman.d/hooks
sudo tee /etc/pacman.d/hooks/90-suid-cleanup.hook > /dev/null << 'EOF'
[Trigger]
Type = Path
Operation = Install
Operation = Upgrade
Target = usr/bin/chfn
Target = usr/bin/chsh

[Action]
Description = Retirando bit SUID de binarios no necesarios...
When = PostTransaction
Exec = /usr/bin/chmod u-s /usr/bin/chfn /usr/bin/chsh
EOF

Qué hace:

  • [Trigger]: define cuándo se ejecuta el hook. En este caso, después de instalar o actualizar los paquetes que contienen chfn o chsh.
  • [Action]: define qué hace. Ejecuta el chmod para quitar el SUID después de que el paquete se haya actualizado.
💡
pkexec se queda. Es un binario SUID necesario para que GNOME pueda solicitar privilegios de forma gráfica. Tiene un historial de vulnerabilidades (como PwnKit), pero no hay alternativa práctica si usas GNOME. Anótalo como excepción aceptada y mantén el sistema actualizado.

13.3 Revisar servicios y puertos abiertos

Antes de aplicar más capas de endurecimiento, conviene hacer una auditoría de lo que está corriendo en el sistema. Este paso sirve para detectar servicios innecesarios que se hayan activado por defecto y para confirmar que no hay nada escuchando en la red que no debería.

Primero, lista los servicios que están en ejecución:

bash
systemctl list-units --type=service --state=running

Qué hace:

  • systemctl list-units: muestra las unidades de systemd actualmente cargadas.
  • --type=service: filtra solo por servicios, excluyendo timers, sockets y otros tipos de unidad.
  • --state=running: muestra solo los que están activos y en ejecución en este momento.

En un sistema recién instalado y bien configurado, la lista debe ser corta. En este caso verás iwd, systemd-networkd, systemd-resolved, systemd-journald, dbus y poco más. Si aparece algo que no reconoces, investígalo con systemctl status nombre.service antes de desactivarlo.

Ahora comprueba qué servicios están habilitados para arrancar automáticamente:

bash
systemctl list-unit-files --state=enabled

Qué hace:

  • list-unit-files: lista todos los archivos de unidad instalados en el sistema.
  • --state=enabled: filtra solo los que están configurados para arrancar automáticamente al iniciar el sistema.

La diferencia con el comando anterior es importante: un servicio puede estar habilitado (arranca solo) pero no en ejecución (porque ha fallado o se ha detenido), o al revés. Aquí interesa ver todo lo que está habilitado, porque eso es lo que arrancará en el próximo reinicio.

Por último, revisa los puertos de red que están a la escucha:

bash
ss -tulpn

Qué hace:

  • ss: herramienta para inspeccionar sockets de red. Sustituye al antiguo netstat.
  • -t: muestra sockets TCP.
  • -u: muestra sockets UDP.
  • -l: muestra solo los sockets en escucha (listening), no las conexiones establecidas.
  • -p: muestra el proceso asociado a cada socket.
  • -n: muestra direcciones y puertos en formato numérico, sin resolver nombres.

En este punto, la salida debe mostrar únicamente systemd-resolved escuchando en 127.0.0.53 y en 127.0.0.54, que son direcciones de loopback (solo accesibles desde el propio equipo). También verás el puerto 5353 (mDNS) escuchando en todas las interfaces; esto es normal ahora mismo porque systemd-resolved lo activa por defecto y aún no lo hemos desactivado. Lo haremos en el bloque siguiente, al configurar la resolución DNS.

Si aparece cualquier otro puerto escuchando en 0.0.0.0 o en [::], investígalo con systemctl status antes de continuar. La política final del sistema es no tener ningún servicio accesible desde la red.

13.4 Umask y límites

La umask define los permisos con los que se crean los archivos nuevos. Por defecto en Arch es 022, lo que significa que los archivos se crean legibles por cualquiera. En un sistema monousuario, no hay motivo para eso.

bash
sudo sed -i 's/^UMASK.*/UMASK\t\t077/' /etc/login.defs
echo "umask 077" | sudo tee /etc/profile.d/99-umask.sh > /dev/null
sudo mkdir -p /etc/systemd/system.conf.d
echo -e "[Manager]\nUMask=0077" | sudo tee /etc/systemd/system.conf.d/umask.conf > /dev/null
sudo chmod 700 /home/*

Qué hace cada uno:

  • sed -i: edita /etc/login.defs en sitio. El patrón reemplaza la línea que empieza por UMASK por UMASK 077.
  • echo "umask 077" | tee: escribe la línea en un script dentro de /etc/profile.d/, que se ejecuta al iniciar sesión. Así la umask se aplica también a las sesiones interactivas.
  • umask.conf: configuración para systemd. Todos los servicios lanzados por systemd usarán esa umask.
  • chmod 700 /home/*: restringe los directorios personales para que solo el propietario pueda acceder a ellos.

Configura también los límites de recursos:

bash
sudo mkdir -p /etc/security/limits.d/
sudo tee /etc/security/limits.d/99-hardening.conf > /dev/null << 'EOF'
*       hard    core        0
*       soft    core        0
*       hard    nproc       8192
*       hard    nofile      16384
*       hard    maxlogins   3
root    hard    nproc       unlimited
EOF

Qué hace cada línea:

  • hard core 0: impide que se generen volcados de memoria (core dumps). Un core dump puede contener contraseñas o claves en memoria.
  • soft core 0: aplica el mismo límite como valor por defecto.
  • nproc 8192: limita el número de procesos que un usuario puede crear. Un valor muy bajo rompería contenedores y VMs, pero 8192 es holgado.
  • nofile 16384: limita el número de descriptores de archivo abiertos simultáneamente. Las bases de datos y los contenedores abren muchos.
  • maxlogins 3: limita a tres las sesiones simultáneas por usuario. Suficiente para uso normal, disuade ataques de fuerza bruta que intenten abrir muchas sesiones.
  • root hard nproc unlimited: exime a root del límite de procesos, para que nunca se quede sin poder lanzar tareas del sistema.
💡
Los valores son más altos que en guías genéricas. Con 8192 procesos y 16384 descriptores, tienes margen suficiente para ejecutar varias máquinas virtuales y contenedores a la vez sin tocar los límites. En un servidor pequeño o en un equipo de escritorio sin virtualización, valores menores serían suficientes, pero aquí no queremos cuellos de botella.

2.99.14 Kernel y controles de seguridad (Fase 2)

En esta fase se ajustan los parámetros del kernel, se restringe la carga de módulos innecesarios y se activan los perfiles de AppArmor. Todo lo que se aplica aquí reduce la superficie de ataque del sistema en tiempo de ejecución, sin tocar el arranque ni el cifrado.

14.1 Parámetros del kernel con sysctl

Los parámetros sysctl permiten modificar el comportamiento del kernel en caliente. Se agrupan en archivos dentro de /etc/sysctl.d/ para que se apliquen al arrancar y sean fáciles de mantener.

Crea el archivo principal con los ajustes de seguridad:

bash
sudo tee /etc/sysctl.d/99-hardening.conf > /dev/null << 'EOF'
# Exposicion de informacion
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.printk = 3 3 3 3
kernel.perf_event_paranoid = 3

# Depuracion y trazas
kernel.yama.ptrace_scope = 1
kernel.sysrq = 4

# Carga de codigo
kernel.kexec_load_disabled = 1

# BPF sin privilegios
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2

# userfaultfd
vm.unprivileged_userfaultfd = 0

# Volcados de memoria
kernel.core_pattern = |/bin/false
fs.suid_dumpable = 0

# Proteccion de enlaces
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2

# Varios
kernel.randomize_va_space = 2
dev.tty.ldisc_autoload = 0
vm.mmap_rnd_bits = 32
EOF

Qué hace cada parámetro:

  • kernel.kptr_restrict = 2: oculta las direcciones del kernel en /proc/kallsyms y en otros sitios, incluso para root. Dificulta exploits que necesitan conocer direcciones de memoria del kernel.
  • kernel.dmesg_restrict = 1: impide que usuarios sin privilegios lean el buffer de mensajes del kernel (dmesg). Evita fugas de información sobre el hardware y el arranque.
  • kernel.printk = 3 3 3 3: limita los mensajes que van a consola. Reduce el ruido y evita que información sensible se muestre en pantallas compartidas.
  • kernel.perf_event_paranoid = 3: restringe el uso de perf_event_open, que permite monitorizar el rendimiento del sistema y, en manos maliciosas, extraer información de otros procesos.
  • kernel.yama.ptrace_scope = 1: limita ptrace a procesos hijos. Impide que un proceso lea la memoria de otro del mismo usuario. Es el valor recomendado para depurar scripts sin abrir la puerta a ataques entre procesos.
  • kernel.sysrq = 4: limita las combinaciones de teclas SysRq a las de solo lectura. Evita que alguien pueda reiniciar o volcar memoria con combinaciones de teclado.
  • kernel.kexec_load_disabled = 1: desactiva kexec, que permite cargar un kernel alternativo sin reiniciar. Es un vector clásico para saltarse Secure Boot. Como usamos lockdown, esta opción refuerza la protección.
  • kernel.unprivileged_bpf_disabled = 1: impide que usuarios sin privilegios carguen programas BPF. Cierra un vector de ataque del kernel.
  • net.core.bpf_jit_harden = 2: endurece el compilador JIT de BPF. Añade aleatorización y comprobaciones extra para dificultar exploits.
  • vm.unprivileged_userfaultfd = 0: desactiva userfaultfd para usuarios sin privilegios. Es un mecanismo que se ha usado en exploits de kernel recientes.
  • kernel.core_pattern = |/bin/false: cuando un proceso recibe una señal de fallo, en lugar de generar un volcado en disco, redirige la salida a /bin/false, que no hace nada. Evita que datos sensibles de la memoria acaben en archivos de volcado.
  • fs.suid_dumpable = 0: impide que los binarios con SUID generen volcados de memoria.
  • fs.protected_symlinks = 1: evita ataques de enlaces simbólicos en directorios compartidos como /tmp.
  • fs.protected_hardlinks = 1: evita ataques de enlaces duros a archivos que no te pertenecen.
  • fs.protected_fifos = 2: protege las tuberías (FIFO) frente a escrituras no autorizadas.
  • fs.protected_regular = 2: protege los archivos regulares en directorios compartidos.
  • kernel.randomize_va_space = 2: activa la aleatorización completa del espacio de direcciones (ASLR).
  • dev.tty.ldisc_autoload = 0: impide que se carguen dinámicamente disciplinas de línea de terminal. Reduce superficie de ataque.
  • vm.mmap_rnd_bits = 32: aumenta los bits de aleatorización en mmap. Hace más difícil predecir direcciones de memoria.
💡
Sobre ptrace_scope = 1: no se usa 2 ni 3 porque romperían la depuración de scripts de Python o Bash. El valor 1 ya cierra el vector real: un proceso no puede inspeccionar la memoria de otro que no sea su hijo.

Ahora los parámetros de red:

bash
sudo tee /etc/sysctl.d/99-network.conf > /dev/null << 'EOF'
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_rfc1337 = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv6.conf.all.use_tempaddr = 2
net.ipv6.conf.default.use_tempaddr = 2
EOF

Qué hace cada parámetro:

  • tcp_syncookies = 1: activa las cookies SYN para mitigar ataques de denegación de servicio por inundación SYN.
  • tcp_rfc1337 = 1: protección contra ataques de tipo TIME_WAIT.
  • rp_filter = 1: activa el filtrado de rutas inverso. Evita paquetes con direcciones IP falsificadas.
  • accept_redirects = 0 (IPv4 e IPv6): ignora los mensajes ICMP de redirección. Evita que un atacante redirija el tráfico.
  • send_redirects = 0: no envía redirecciones ICMP. El equipo no actúa como router.
  • accept_source_route = 0: ignora las rutas de origen especificadas en los paquetes.
  • log_martians = 1: registra los paquetes con direcciones imposibles. Útil para detectar escaneos.
  • icmp_echo_ignore_broadcasts = 1: ignora peticiones de eco ICMP dirigidas a direcciones de broadcast. Evita que el equipo responda a ataques de amplificación.
  • icmp_ignore_bogus_error_responses = 1: ignora respuestas ICMP de error mal formadas.
  • use_tempaddr = 2: usa direcciones IPv6 temporales para conexiones salientes. Dificulta el seguimiento.
⚠️
No se desactiva IPv6. Es un antipatrón habitual pero rompe compatibilidad con muchas redes modernas. Los parámetros anteriores ya mitigan los riesgos asociados.

Aplica todos los cambios:

bash
sudo sysctl --system

Qué hace:

  • sysctl --system: lee y aplica todos los archivos de configuración de /etc/sysctl.d/ y /etc/sysctl.conf. Muestra por pantalla cada parámetro aplicado.

Desactiva también los volcados de memoria del sistema:

bash
sudo mkdir -p /etc/systemd/coredump.conf.d
sudo tee /etc/systemd/coredump.conf.d/disable.conf > /dev/null << 'EOF'
[Coredump]
Storage=none
ProcessSizeMax=0
EOF

Qué hace cada opción:

  • Storage=none: no guarda ningún volcado de memoria.
  • ProcessSizeMax=0: no permite que se generen volcados de ningún tamaño.

14.2 Bloquear módulos innecesarios

No se trata de bloquear todo indiscriminadamente, sino solo aquellos módulos que no se usan y que amplían la superficie de ataque. La lista es concreta:

bash
sudo tee /etc/modprobe.d/blacklist-hardening.conf > /dev/null << 'EOF'
# Sistemas de ficheros poco auditados
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
install hfs /bin/true
install hfsplus /bin/true
install udf /bin/true

# Protocolos de red obsoletos
install dccp /bin/true
install sctp /bin/true
install rds /bin/true
install tipc /bin/true
install n-hdlc /bin/true
install ax25 /bin/true
install netrom /bin/true
install x25 /bin/true
install rose /bin/true
install decnet /bin/true
install econet /bin/true
install ipx /bin/true
install appletalk /bin/true
install psnap /bin/true
install p8023 /bin/true
install p8022 /bin/true
install can /bin/true
install atm /bin/true

# Buses con acceso DMA que no tienes
install firewire-core /bin/true
install firewire-ohci /bin/true
install thunderbolt /bin/true

# Bluetooth (mismo chip AX200, pero dispositivo USB aparte)
install btusb /bin/true
EOF

Qué hace cada línea:

  • install modulo /bin/true: le dice al kernel que, en lugar de cargar el módulo, ejecute /bin/true, que no hace nada. Es la forma correcta de bloquear un módulo: no se carga pero tampoco da error.
⚠️
Módulos que NO se bloquean y por qué:
  • squashfs: lo necesitan las imágenes de contenedor y los AppImage.
  • overlay: es el sistema de ficheros que usa Podman.
  • thunderbolt: sí se bloquea porque el diagnóstico confirmó que no hay controlador Thunderbolt. Si algún día se conecta un dock USB-C con TB, quitar esa línea.
  • btusb: se bloquea, pero si se quieren usar auriculares Bluetooth, quitar la línea y usar rfkill block bluetooth.

Regenera la initramfs para que los cambios en modprobe.d se apliquen desde el arranque:

bash
sudo mkinitcpio -P
sudo sbctl verify

Qué hace cada uno:

  • mkinitcpio -P: regenera las UKI con la nueva configuración de módulos. El hook modconf incluye los archivos de /etc/modprobe.d/ en la initramfs.
  • sbctl verify: comprueba que las UKI siguen firmadas correctamente después de regenerarlas.

14.3 AppArmor con perfiles reales

AppArmor ya está activo (lo vimos en el bloque anterior). Lo que falta es cargar perfiles útiles. En este punto, la mayoría de procesos no están confinados, así que la protección real es mínima.

Comprueba el estado actual:

bash
sudo aa-status

Qué hace:

  • aa-status: muestra información sobre AppArmor: si está activo, cuántos perfiles hay cargados, cuántos procesos están confinados y en qué modo (enforce o complain).

El número que importa es procesos confinados, no perfiles cargados. Un perfil cargado no sirve de nada si no hay procesos que lo usen.

Los perfiles a poner en enforce por orden de exposición son: navegador, cliente de correo y lector de PDF. Para ellos, se pueden usar los perfiles que vienen en el paquete apparmor o crearlos con aa-genprof.

bash
sudo aa-status | grep -E "processes are in (enforce|complain)"
💡
Es normal que en este punto aparezca 0 en ambas líneas. El paquete base apparmor en Arch solo proporciona el motor de seguridad, no perfiles predefinidos. Además, todavía no hemos instalado aplicaciones que merezcan ser confinadas. Los perfiles se añadirán más adelante, cuando el sistema tenga el entorno gráfico y el navegador instalados.

Qué hace:

  • Filtra la salida de aa-status para mostrar solo cuántos procesos están en modo enforce (bloqueando) y cuántos en complain (solo registrando).
⚠️
No aplicar perfiles de AppArmor a podman, crun ni qemu sin entender bien lo que se hace. Confinar el runtime de contenedores rompe el aislamiento en lugar de reforzarlo. Podman ya aplica su propio perfil containers-default a los procesos de dentro. Lo mismo aplica a libvirtd y sus servicios asociados.

2.99.15 Usuarios y privilegios (Fase 3)

En esta fase se endurece la gestión de privilegios: se configura sudo con opciones seguras, se aplican políticas de contraseñas y bloqueo de cuentas, y se deshabilita la cuenta de root. Es una fase delicada porque un error en la configuración de PAM o sudo puede dejarte fuera del sistema.

🚨
Antes de empezar, abre una segunda terminal como root (Ctrl+Alt+F2, inicia sesión como root) y no la cierres hasta terminar esta fase. Si cometes un error en la configuración de sudo o PAM, esa terminal te permitirá corregirlo sin tener que arrancar desde el USB. Una vez que todo funcione, puedes cerrarla.

15.1 Verificar sudo antes de tocar nada

Comprueba que sudo funciona correctamente con tu usuario antes de hacer cambios:

bash
sudo -v && echo "sudo OK"

Qué hace:

  • sudo -v: actualiza el timestamp de sudo. Si tu usuario tiene permisos, pedirá la contraseña y la validará durante unos minutos.
  • && echo "sudo OK": solo se ejecuta si el comando anterior ha tenido éxito. Si ves "sudo OK", todo está bien.

Si esto falla, no continúes. Revisa que tu usuario esté en el grupo wheel y que la línea %wheel ALL=(ALL:ALL) ALL esté descomentada en /etc/sudoers. Si no puedes arreglarlo, usa la terminal root que abriste al principio.

15.2 Bloquear la cuenta de root

Una vez confirmado que sudo funciona, se puede bloquear la cuenta de root. Esto impide el inicio de sesión directo como root y obliga a usar sudo con tu usuario, lo que deja un registro de las acciones.

bash
sudo passwd -l root
sudo passwd -S root

Qué hace cada uno:

  • passwd -l root: bloquea la cuenta de root añadiendo un ! al principio del hash de la contraseña en /etc/shadow. El login como root queda deshabilitado.
  • passwd -S root: muestra el estado de la cuenta. Debe aparecer una L (Locked) en la segunda columna.
⚠️
No bloquees root antes de verificar que sudo funciona. Si sudo falla y root está bloqueado, te quedarás fuera del sistema. La segunda terminal root que abriste al principio es tu red de seguridad.

15.3 Configurar sudo con opciones seguras

Ahora se añaden opciones de seguridad a sudo. En lugar de editar el archivo principal, se usa un archivo dentro de /etc/sudoers.d/ para mantener las personalizaciones separadas:

bash
sudo EDITOR=vim visudo -f /etc/sudoers.d/99-hardening

Qué hace:

  • visudo -f: edita un archivo específico con la validación de sintaxis de visudo. Si cometes un error, no guardará los cambios.
  • EDITOR=vim: fuerza a visudo a usar vim en lugar del editor por defecto.

Añade el siguiente contenido:

bash
Defaults    use_pty
Defaults    logfile=A
Defaults    log_input, log_output
Defaults    iolog_dir=A
Defaults    timestamp_timeout=5
Defaults    passwd_timeout=1
Defaults    passwd_tries=3
Defaults    !visiblepw
Defaults    always_set_home
Defaults    env_reset
Defaults    secure_path=A
Defaults    lecture=once

Qué hace cada opción:

  • use_pty: obliga a que los comandos se ejecuten en una pseudo-terminal propia. Evita que un proceso malicioso que sobreviva a sudo se quede con tu terminal para inyectar comandos.
  • logfile="/var/log/sudo.log": registra los comandos de sudo en un archivo aparte.
  • log_input, log_output: graba la entrada y la salida de los comandos ejecutados con sudo. Permite auditar exactamente qué se hizo.
  • iolog_dir: directorio donde se guardan los registros de entrada/salida, organizados por usuario.
  • timestamp_timeout=5: los privilegios de sudo caducan a los 5 minutos. Evita que una sesión desatendida mantenga privilegios indefinidamente.
  • passwd_timeout=1: espera un minuto como máximo para introducir la contraseña.
  • passwd_tries=3: permite tres intentos antes de fallar.
  • !visiblepw: no muestra la contraseña mientras se escribe.
  • always_set_home: establece HOME al directorio del usuario que ejecuta sudo.
  • env_reset: limpia las variables de entorno antes de ejecutar el comando. Evita que variables maliciosas afecten al comando con privilegios.
  • secure_path: define un PATH seguro para los comandos ejecutados con sudo.
  • lecture=once: muestra el mensaje de advertencia de sudo solo la primera vez.
💡
No se añade requiretty. Esa opción rompe los scripts que llaman a sudo sin terminal, algo que haremos en fases posteriores. use_pty ya proporciona la protección que importa.

Restringe también el uso de su al grupo wheel. Edita los archivos de PAM:

bash
sudo vim /etc/pam.d/su

Busca la línea:

bash
# auth        required    pam_wheel.so use_uid

Y descoméntala (quita el #). Haz lo mismo en /etc/pam.d/su-l:

bash
sudo vim /etc/pam.d/su-l

Qué hace:

  • pam_wheel.so use_uid: solo los usuarios del grupo wheel pueden usar su para cambiar a otro usuario (incluido root). Refuerza el control de privilegios.

15.4 Política de contraseñas con libpwquality

Instala libpwquality para aplicar reglas de complejidad a las contraseñas:

bash
sudo pacman -S libpwquality

Qué hace:

  • libpwquality: biblioteca que proporciona comprobaciones de calidad de contraseñas. Se integra con PAM para rechazar contraseñas débiles al cambiarlas.

Configura las reglas:

bash
sudo vim /etc/security/pwquality.conf

Deja el archivo así:

bash
minlen = 12
minclass = 3
maxrepeat = 3
maxsequence = 4
dictcheck = 1
usercheck = 1
enforcing = 1
retry = 3

Qué hace cada opción:

  • minlen = 12: longitud mínima de 12 caracteres.
  • minclass = 3: exige al menos tres tipos de caracteres distintos (mayúsculas, minúsculas, dígitos, símbolos).
  • maxrepeat = 3: no permite más de tres caracteres idénticos consecutivos.
  • maxsequence = 4: no permite secuencias de más de cuatro caracteres (por ejemplo, abcd o 1234).
  • dictcheck = 1: comprueba que la contraseña no esté en un diccionario de contraseñas comunes.
  • usercheck = 1: rechaza contraseñas que contengan el nombre de usuario.
  • enforcing = 1: aplica las reglas de forma obligatoria.
  • retry = 3: permite tres intentos antes de fallar el cambio de contraseña.

15.5 Bloqueo de cuentas con faillock

Configura faillock para bloquear cuentas tras varios intentos fallidos de inicio de sesión:

bash
sudo vim /etc/security/faillock.conf

Añade o modifica:

bash
deny = 5
fail_interval = 900
unlock_time = 600
even_deny_root
root_unlock_time = 600
audit
silent

Qué hace cada opción:

  • deny = 5: bloquea la cuenta tras cinco intentos fallidos.
  • fail_interval = 900: cuenta los intentos fallidos en una ventana de 15 minutos.
  • unlock_time = 600: desbloquea automáticamente después de 10 minutos.
  • even_deny_root: aplica las reglas también a root.
  • root_unlock_time = 600: tiempo de desbloqueo para root.
  • audit: registra los bloqueos en el sistema de auditoría.
  • silent: no informa al usuario sobre el bloqueo (dificulta la enumeración de usuarios).

Comprueba que las líneas de faillock están activas en PAM:

bash
grep faillock /etc/pam.d/system-auth

Qué hace:

  • Busca las líneas que hacen referencia a faillock en el archivo de autenticación del sistema. Deben aparecer al menos dos: una con preauth y otra con authfail.
⚠️
No edites system-auth a ciegas. Lo gestiona el paquete pam y un error puede bloquear el login local y el gráfico a la vez. Si necesitas hacer cambios, usa los archivos de /etc/security/ y deja que PAM los lea.

Prueba el sistema de bloqueo desde la terminal root que dejaste abierta:

bash
faillock --user cambiame

Qué hace:

  • Muestra los intentos fallidos registrados para el usuario. Si no hay ninguno, aparecerá la lista vacía.
bash
faillock --user cambiame --reset

Qué hace:

  • Reinicia el contador de intentos fallidos para ese usuario.

15.6 Algoritmo de hash de contraseñas

Verifica que el sistema usa yescrypt como algoritmo de hash para las contraseñas. Es el más moderno y resistente a ataques con GPU:

bash
grep -E "^ENCRYPT_METHOD" /etc/login.defs

Qué hace:

  • Busca la línea que define el método de cifrado de contraseñas. Debe mostrar ENCRYPT_METHOD YESCRYPT.

Regenera el hash de tu contraseña para que se aplique el algoritmo actual:

bash
sudo passwd cambiame

Qué hace:

  • passwd: cambia la contraseña del usuario. Al introducir la nueva, se generará un hash con el método configurado.

Comprueba que el hash usa yescrypt:

bash
sudo awk -F: '$2 ~ /^\$y\$/ {print $1": yescrypt OK"}' /etc/shadow

Qué hace:

  • Busca en /etc/shadow los hashes que empiezan por $y$, que es el prefijo de yescrypt. Si aparece tu usuario con "yescrypt OK", el cambio se ha aplicado correctamente.

15.7 Comprobación final

Antes de cerrar la terminal root de seguridad, verifica que puedes iniciar sesión con tu usuario y que sudo sigue funcionando:

bash
sudo -v && echo "sudo OK"

Qué hace:

  • Vuelve a validar sudo. Debe pedirte la contraseña y funcionar sin problemas.

Si todo está correcto, puedes cerrar la terminal root (escribe exit). A partir de ahora, toda la administración se hará desde tu usuario con sudo.