Seguridad
En este capítulo hablaremos de la seguridad operacional, táctica y estratégica de la información.
Lo veremos a nivel de implementación (y por ende; práctico).
Sistemas de seguridad en Linux
Linux utiliza varios métodos de seguridad, que puede verificarse cuales están en funcionamiento viendo el archivo; “/sys/kernel/security/lsm”
-
Control de acceso discrecional de Unix (DAC)
Este es el más básico de todos.
Funciona estableciendo unos permisos en un directorio ó archivo de; quien es el propietario y a que grupo pertenece (el directorio ó archivo). Una vez que está configurado eso, se encarga de establecer un sistema de permisos sobre tales;
- leer (read), abreviado como “r” ó con el número “4”.
- escribir (write), abreviado como “w” ó con el número “2”.
- ejecutar (execute), abreviado como “x” ó con el número “1”.
Nota Las carpetas/directorios son simplemente archivos que contienen los metadatos de que archivos y otros directorios “contienen” dentro. Simplemente hacen referencias que de cara al usuario son transparentes y al kernel le sirven para saber que debe o no devolver. Por eso mismo todos los directorios deben tener permisos de ejecución para leer lo que tienen internamente Por ende, para incluir también aquellos usuarios (o grupos de usuarios) que no pertenecen al propietario ni a su grupo (osea; otros), se organiza de la siguiente manera;
- Para permisos por letras;
[permisos_propietario][permisos_grupo][permisos_otros]
- Para permisos por números;
[sumatoria_centenas_propietario][sumatoria_decenas_grupo][sumatoria_unidades_otros]
Veamos un ejemplo;
Como se puede evidenciar tenemos una carpeta (fijate en la “d” adelante de todo) con el nombre “linux-6.8.8” y un archivo (fijate que no tiene la “d” adelante de todo) llamado “linux-6.8.8.tar.xz”.
El comando “ls” muestra los permisos de la carpeta como el archivo en formato de letras. Por ende si nos fijamos en el archivo comprimido con el algoritmo “xz” vemos la siguiente estructura; “-rw-r--r--”
Recuerde el orden, por lo que podemos ver que el propietario (el usuario ‘root’ que primero muestra ahí) tiene permisos de lectura (‘r’) y de escritura (‘w’) pero no de ejecución (de ahí el “-”). El grupo de usuarios de ese archivo (el segundo ‘root’ que aparece) tiene permisos solamente de lectura (‘r’) pero no se escritura ni ejecución (de ahí los “--”). Por último, los que no sean usuario root ni tampoco estén en el grupo del mismo nombre tienen permisos de solamente lectura, no de escritura ni ejecución.
Si estos mismos permisos los pasamos a una sumatoria numeral quedaría; 644.
Esto permite aislar carpetas de distintos usuarios propietarios.
Dato importante Algunos sistemas de archivos permiten que ciertas particiones se monten deshabilitando la ejecución de programas binarios desde ahí, aumentando la seguridad. -
SELinux (Security Enhanced Linux)
Es un módulo de seguridad del kernel Linux, dentro de la categoría MAC (Mandatory Access Control).
Fue creado por la NSA y Red Hat el 22-12-2000 e introducido en la versión 2.6 del núcleo el 08-08-2003.
Utiliza reglas (almacenadas en; “/usr/share/selinux/targeted”) para identificar que recursos necesita la aplicación, a qué dispositivos físicos (en “/dev”) puede acceder, que permisos va a tener sobre determinados directorios, si puede o no conectarse a internet, etc. De esta manera todo lo que no esté permitido o especificado en esas reglas le hes denegado a la aplicación.
Internamente puede estar aplicando las políticas de las reglas (modo; “enforced”) o monitorear que pasa, independientemente de lo que indiquen las reglas (modo; “permissive”).
Como puede haber binarios (programas ejecutables) en muchas posibles ubicaciones, SELinux utiliza un sistema de etiquetas (labels) sobre el binario (por ende el sistema de archivos debe soportar las etiquetas) para que cuando se ejecute pueda identificar desde el inicio que política debe aplicar. Sin importar si el binario cambia de ubicación a futuro, usará la misma política. Pero no solamente apunta a binarios, si no a todo componente del sistema operativo.
Este sistema de MAC tiene la ventaja de que permite reglas mucho más completas y complejas que el esquema de permisos Unix tradicional.
SELinux tiene la desventaja de ser dificil de aprender a manejar y administrar.
Lo utilizan por defecto; Fedora, RedHat Enterprise, CentOS, Rocky, Scientific y Android (desde la versión 5.0).
Nota Kubernetes no es totalmente compatible con este sistema de seguridad y por eso pide que lo deshabilites al momento de instalarlo. -
AppArmor
Es un módulo de seguridad del kernel Linux, dentro de la categoría MAC (Mandatory Access Control).
Fue creado por Immunix en 1998 y desde 2009 lo mantiene Canonical (la creadora de Ubuntu Linux).
Utiliza reglas (almacenadas en; “/etc/apparmor.d”) para identificar que recursos necesita la aplicación, a qué dispositivos físicos (en “/dev”) puede acceder, que permisos va a tener sobre determinados directorios, si puede o no conectarse a internet, etc. De esta manera todo lo que no esté permitido o especificado en esas reglas le hes denegado a la aplicación.
Internamente puede estar aplicando las políticas de las reglas (modo; “enforce”) o monitorear que pasa, independientemente de lo que indiquen las reglas (modo; “complain”).
A diferencia de SELinux que aplica etiquetas directamente en el binario, apparmor se basa en la especificación del path absoluto del binario en sus propias reglas para saber que política debe implementar o no. Esto ocasiona que si el binario cambia de ubicación, la política no se aplicará pero también es muchísimo más fácil de administrar que SELinux y evita problemas de compatibilidad (a nivel seguridad) en puntos de montaje NFS, Samba, etc.
Lo utilizan por decto; Debian, Ubuntu, Mint, SUSE, Arch, Alpine, entre otros.
-
Tomoyo
Es un módulo de seguridad del kernel Linux, dentro de la categoría MAC (Mandatory Access Control).
Fue creado en el 2003 y esponsoreado por la empresa japonesa NTT Data hasta 2012, siendo fusionado en el núcleo en el 2009 (v2.6.30).
Es muy parecido en funcionamiento a SELinux, ya que apunta a proteger todo componente del sistema operativo pero no tiene tanto desarrollo como este.
Debido a que habia nacido como una serie de parches a ser aplicados, para ser fusionado con el núcleo se debía implementar una enorme cantidad de cambios adicionales. Esto ocasionó que el desarrollo se dividiera en dos partes; la rama de versionado v1.X (que sigue mediante parches) y la rama de versionado v2.X (que está integrada dentro del núcleo). De igual manera se hizo un fork de la rama v1.X llamado Akari como módulo de seguridad en el kernel.
En el versionado v1, las reglas se encuentran almacenadas en; “/etc/css”. Así mismo el init no puede ser SystemD u OpenRC si no que debe ser “/usr/bin/ccs-init”.
En el versionado v2, las reglas se encuentran almacenadas en; “/etc/tomoyo/policy”. En esta versión el init puede ser SystemD, OpenRC o el que se utilice.
Al igual que AppArmor se basa en el path absoluto de ejecución.
-
YAMA
Es un módulo de seguridad del kernel Linux, dentro de la categoría MAC (Mandatory Access Control).
Se basa en la determinación de que política aplicar según el proceso que esté en ejecución. Permite una mayor libertad y versatilidad que AppArmor pero no tanto como SELinux. Este tipo de aproximación es para evitar que un proceso vulnerable comprometido pueda hacer una adjunción (“attach”) a otro proceso legítimo, en los Linux esto se hace principalmente a través de “ptrace”.
Tiene 4 modos; 0 (deshabilitado), 1 (‘restricted’) para solamente permitir ptrace con sudo/doas, 2 (‘admin-only’) para permitir ptrace solamente como usuario root, 3 (‘ptrace-disabled’) para no permitir debug alguno.
-
Lockdown
Este es un modo introducido en la release 5.4 del núcleo.
Viene a endurecer el perimetro (“boundary”) entre el usuario root (User Id -uid-: 0) y el propio núcleo. De esta manera en caso de que el usuario root quiera modificar el núcleo del sistema en ejecución se puede permitir o denegar esto. Algunos ejemplos seria;
- Uso de kexec
Permite arrancar un nuevo kernel desde uno ya existente sin tener que reiniciar.
- eBPF (Extended Berkeley Packet Filter)
eBPF es una tecnología que permite que sin tener que modificar el código del núcleo o cargar diferentes módulos, se pueda ejecutar programas en un contexto privilegiado al mismo nivel que el kernel.
Entonces, tanto kexec como eBPF pueden ser realmente peligrosos en un sistema con información crítica (ejemplos; la computadora que se encarga de registrar los pulsos eléctricos del latido de corazón de un paciente, un sistema de soporte vital en un satélite artificial, el procesamiento en un F1 manejado por IA, etc).
Por lo que para saber si el usuario root puede o no realizar modificaciones en el espacio del núcleo tenemos tres modos; “none” que permite modificar, “integrity” que no permite modificaciones y por último, “confidentiality” que además no permite extraer información sensible desde el núcleo.
Personalmente “integrity” suele ser el punto adecuado para la mayoría, ya que a veces “confidentiality” es muy agresivo, pero tiene que probar y verificar. Considerar que lo que indicó Linus Torvalds en su momento hace unos años; “las aplicaciones que dependen del acceso a bajo nivel al hardware o al kernel pueden dejar de funcionar”
El modo se setea indicando el argumento “lockdown=[mode]” al kernel (CMDLINE). Aunque si en un sistema en funcionamiento iniciado con “none”, se le indica esto se puede cambiar a un modo más restrictivo (pero no a la inversa);
echo confidentiality > /sys/kernel/security/lockdown
Nota |
---|
Con lockdown habilitado no se pueden cargar módulos fuera del kernel embebido, por lo que los drivers de VirtualBox, NVIDIA y otros que estén como módulos no se iniciarán. Esto es excelente cuando se tiene todo dentro del kernel embebido pero una molestia cuando se requiere usar módulos. |
-
Landlock
Es un módulo de seguridad del kernel Linux.
Persigue un mismo tipo de funcionalidad que Tomoyo pero con el añadido de poder funcionar en conjunto con otros LSM (Linux Security Modules).
- A diferencia de AppArmor y SELinux al aplicar una regla se hereda a los procesos hijos que pueda ejecutar, en vez de separar cada uno según regla.
- A diferencia de AppArmor pero en semejanza con SELinux, Landlock tiene capacidad de aplicar reglas de redes.
- A semejanza de AppArmor y SELinux, hay reglas sobre el sistema de archivos aplicadas (o no) a un objeto.
Si te encuentras cómodo con C y C++, con landlock te sentirás como pez en el agua ya que su sintaxis de regla es muy parecida.
-
Funciones criptográficas y soporte
Las funciones criptogŕaficas del núcleo son provistas mediante una A.P.I. accesible desde la librería estándar de C que se esté usando; glibc, musl, etc.
Las funciones criptográficas proveen, muchas veces, una base esencial para el resto de sus funciones de seguridad (no solamente los módulos y modos anteriormente mencionados)
En el kernel con versionado 6.8.8 tenemos (entre muchos otros ‘features’ disponibles adicionales);
- Motor (engine) paralelo para funciones criptográficas.
- Configuraciones criptográficas para utilizar las funciones por parte de procesos no root.
- Soporte para; RSA, DH, ECDH, ECDSA, EC-RDSA, SM2, AES, ARIA, BlowFish, Camellia, CAST5 y CAST6, DES, FCrypt, Serpent, Blake2, SHA (1, 256, 512, etc), LZO, LZ4, ZStandard (zstd), entre muchos otros.
Hardening del userland
Como mencionamos anteriormente el userland es todo el set de herramientas, librerías y archivos que hacen que el usuario pueda utilizar el sistema operativo.
Como tal, elegir el userland básico del sistema es especialmente importante para que la totalidad sea tan estable y minimalista como sea posible.
El set básico de Arch Linux incluye la shell “bash”, el set coreutils, el editor nano y el gestor de paquetes pacman. En concreto el paquete coreutils incluye;
Programa | Propósito |
---|---|
chcon | Cambia el contexto de seguridad para SELinux en un archivo/binario |
chgrp | Cambia la propiedad del grupo del archivo |
chown | Cambia la propiedad del archivo |
chmod | Cambia los permisos de un archivo o directorio |
cp | Copia un archivo o directorio |
dd | Copia y convierte un archivo |
df | Muestra espacio libre en disco en sistemas de archivos |
dir | Es exactamente igual a “ls -C -b”. (Los archivos se enumeran por defecto en columnas y se clasifican verticalmente). |
dircolors | Configura el color para ls |
install | Copia archivos y establece atributos |
ln | Crea un enlace a un archivo |
ls | Enumera los archivos en un directorio |
mkdir | Crea un directorio |
mkfifo | Crea pipes con nombre (FIFOs) |
mknod | Crea archivos especiales de bloque o caracter |
mktemp | Crea un archivo o directorio temporal |
mv | Mueve archivos o renombra archivos |
realpath | Devuelve la ruta absoluta o relativa resuelta para un archivo |
rm | Elimina (borra) archivos, directorios, nodos de dispositivos y enlaces simbólicos |
rmdir | Elimina directorios vacíos |
shred | Sobrescribe un archivo para ocultar su contenido y, opcionalmente, lo elimina |
sync | Libera los búferes del sistema de archivos |
touch | Cambia las marcas de tiempo del archivo; crea archivo |
truncate | Reduce o extiende el tamaño de un archivo al tamaño especificado |
vdir | Es exactamente igual a “ls -l -b”. (Los archivos se enumeran por defecto en formato largo). |
b2sum | Calcula y verifica el resumen del mensaje BLAKE2b |
base32 | Codifica o decodifica Base32 e imprime el resultado en la salida estándar |
cat | Concatena e imprime archivos en la salida estándar |
cksum | Suma de comprobación (IEEE Ethernet CRC-32) y cuenta los bytes en un archivo. Reemplaza a otras utilidades *sum con la opción -a de la versión 9.0. |
comm | Compara dos archivos ordenados línea por línea |
csplit | Divide un archivo en secciones determinadas por líneas de contexto |
cut | Elimina secciones de cada línea de archivos |
expand | Convierte tabuladores en espacios |
fmt | Formateador de texto simple y óptimo |
fold | Envuelve cada línea de entrada para que se ajuste al ancho especificado |
head | Muestra la primera parte de los archivos |
join | Une líneas de dos archivos en un campo común |
md5sum | Calcula y verifica el resumen del mensaje MD5 |
nl | Numera líneas de archivos |
numfmt | Reformatea números |
od | Vuelca archivos en octal y otros formatos |
paste | Combina líneas de archivos |
ptx | Produce un índice permutado del contenido del archivo |
pr | Convierte archivos de texto para imprimir |
sha1sum, sha224sum, sha256sum, sha384sum, sha512sum | Calcula y verifica resúmenes de mensajes SHA-1/SHA-2 |
shuf | genera permutaciones aleatorias |
sort | Ordena líneas de archivos de texto |
split | Divide un archivo en partes |
sum | Suma de comprobación y cuenta los bloques en un archivo |
tac | Concatena e imprime archivos en orden inverso línea por línea |
tail | Muestra la última parte de los archivos |
tr | Traduce o elimina caracteres |
tsort | Realiza una ordenación topológica |
unexpand | Convierte espacios en tabuladores |
uniq | Elimina líneas duplicadas de un archivo ordenado |
wc | Imprime el número de bytes, palabras y líneas en archivos |
arch | Imprime el nombre del hardware de la máquina (igual que uname -m) |
basename | Elimina el prefijo de ruta de un nombre de ruta |
chroot | Cambia el directorio raíz |
date | Imprime o configura la fecha y hora del sistema |
dirname | Elimina el sufijo que no es directorio del nombre del archivo |
du | Muestra el uso del disco en los sistemas de archivos |
echo | Muestra una línea de texto especificada |
env | Muestra y modifica variables de entorno |
expr | Evalúa expresiones |
factor | Descompone números en factores primos |
false | No hace nada, pero sale sin éxito |
groups | Muestra los grupos a los que pertenece el usuario |
hostid | Imprime el identificador numérico del host actual |
id | Imprime UID y GID real o efectivo |
link | Crea un enlace a un archivo |
logname | Imprime el nombre de inicio de sesión del usuario |
nice | Modifica la prioridad de programación |
nohup | Permite que un comando siga ejecutándose después de cerrar la sesión |
nproc | Consulta el número de procesadores (activos) |
pathchk | Comprueba si los nombres de archivo son válidos o portables |
pinky | Una versión ligera de finger |
printenv | Imprime variables de entorno |
printf | Formatea e imprime datos |
pwd | Imprime el directorio de trabajo actual |
readlink | Muestra el valor de un enlace simbólico |
runcon | Ejecuta un comando con el contexto de seguridad especificado |
seq | Imprime una secuencia de números |
sleep | Se retrasa durante un período de tiempo específico |
stat | Devuelve datos sobre un inodo |
stdbuf | Controla el almacenamiento en búfer para comandos que usan stdio |
stty | Cambia e imprime la configuración de la línea de terminal |
tee | Envía la salida a varios archivos |
test | Evalúa una expresión |
timeout | Ejecuta un comando con un límite de tiempo |
true | No hace nada, pero sale con éxito |
tty | Imprime el nombre del terminal |
uname | Imprime información del sistema |
unlink | Elimina el archivo especificado usando la función unlink |
uptime | Indica cuánto tiempo ha estado funcionando el sistema |
users | Imprime los nombres de usuario de los usuarios actualmente conectados en el host actual |
who | Imprime una lista de todos los usuarios actualmente conectados |
whoami | Imprime el userid efectivo |
yes | Imprime una cadena repetidamente |
[ | Un sinónimo de test; este programa permite expresiones como [ expresión ]. |
Como puede evidenciar el paquete chico precisamente no es, y si encima consideramos que muchas herramientas tienen funcionalidades duplicadas (“ls” y “dir”, “wc” y “nl”, “cat” y “nl”, “echo” y “printf”, entre otros) no tenemos precisamente un set minimalista; esto ocasiona una serie de problemas como duplicación de código, mayor consumo de espacio de almacenamiento, mayor superficie de ataque al tener más programas y más dependencias de librerías, mayor probabilidad de bugs en código, y un largo etc.
Por contra, Alpine utiliza BusyBox; es un programa que aglutina como builtins (véase; programas internos que pueden ser llamados desde el exterior) todo el set de coreutils (o la gran mayoría) así como shells, gestores de red, su propia versión de udev (llamada; mdev), y cientos de utilidades más.
BusyBox 1.37.0 |
---|
Para hacer funcionar a cada builtin de forma independiente al momento de instalarse se crean respectivos enlaces simbólicos hacia busybox;
Por lo tanto entramos en una decisión; o usamos una solución todo en uno o usamos muchas soluciones especializadas. Como todo en la vida la respuesta dependerá de la situación y las necesidades, yo personalmente abogo por la utilización de los “coreutils” pero haciendo una compilación para solamente dejar los programas verdaderamente necesarios y no todo el set completo.
Esto se hace mediante la modificación de los flags (opciones de configuración) en el proceso de compilación de los coreutils;
./bootstrap && ./configure --prefix=/usr --libexecdir=/usr/lib \
--with-openssl --enable-no-install-program=[PROGRAMAS_A_NO_INSTALAR_SEPARADOS_POR_COMA]
Con la compilación superior se puede delimitar los programas que serán compilados en el set de coreutils. Sin embargo esto no debe hacerse de forma manual, ya que por defecto los coreutils vienen instalados y una compilación (y posterior instalación) manual sobre escribirá los archivos existentes sin que el gestor de paquetes se entere de esto.
Como tal, se deben crear los archivos de configuración específicos que le indican a “apk”, “pacman” (o el gestor que se use) todo el proceso de compilación y que pueda, posteriormente, hacer un seguimiento de todos los archivos y carpetas involucrados. Este es el archivo PKGBUILD que indica a “pacman” todo el proceso;
# Maintainer: Sébastien "Seblu" Luttringer
# Maintainer: Tobias Powalowski <tpowa@archlinux.org>
# Contributor: Bartłomiej Piotrowski <bpiotrowski@archlinux.org>
# Contributor: Allan McRae <allan@archlinux.org>
# Contributor: judd <jvinet@zeroflux.org>
pkgname=coreutils
pkgver=9.5
pkgrel=1
pkgdesc='The basic file, shell and text manipulation utilities of the GNU operating system'
arch=('x86_64')
license=('GPL-3.0-or-later' 'GFDL-1.3-or-later')
url='https://www.gnu.org/software/coreutils/'
depends=('glibc' 'acl' 'attr' 'gmp' 'libcap' 'openssl')
source=("https://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.xz"{,.sig})
validpgpkeys=('6C37DC12121A5006BC1DB804DF6FD971306037D9') # Pádraig Brady
sha256sums=('cd328edeac92f6a665de9f323c93b712af1858bc2e0d88f3f7100469470a1b8a'
'SKIP')
prepare() {
cd $pkgname-$pkgver
# apply patch from the source array (should be a pacman feature)
local src
for src in "${source[@]}"; do
src="${src%%::*}"
src="${src##*/}"
[[ $src = *.patch ]] || continue
echo "Applying patch $src..."
patch -Np1 < "../$src"
done
}
build() {
cd $pkgname-$pkgver
./configure \
--prefix=/usr \
--libexecdir=/usr/lib \
--with-openssl \
--enable-no-install-program=groups,hostname,kill,uptime
make
}
check() {
cd $pkgname-$pkgver
make check
}
package() {
cd $pkgname-$pkgver
make DESTDIR="$pkgdir" install
}
# vim:set ts=2 sw=2 et:
Nota / Aclaración |
---|
Yo acá indico como usar en pacman y apk pero si a vos te resulta más fácil construir para Debian (apt) o Fedora (dnf) proseguí, lo importante no es la herramienta en si si no que sepas que hacer. |
El archivo superior le indica a pacman; el nombre del programa (pkgname), la versión (pkgver), el número de liberación/release (pkgrel), una descripción del paquete (pkgdesc), la/s arquitectura/s en donde el paquete se puede (arch), la/s licensia/s del mismo (license), la URL del proyecto (url), de que otros programas depende (depends), la url del código fuente (source), las llaves PGP válidas (validpgpkeys), la sumatoria criptográfica hash de todos los archivos fuente (sha256sums), así como que tiene que hacer antes de ponerse a compilar el paquete (prepare), los pasos a seguir para compilar el programa (build), como verificar posteriormente si está todo bien (check) y finalmente los pasos para instalar en un directorio que puede seguir los cambios (package).
Como puede evidenciar, en “build” se indica el “configure” donde se habilita o deshabilita funcionalidades del programa. Esto debe ir de la mano con lo que se especifica en dependencias (depends) ya que si se deshabilita el soporte de acl (“–disable-acl”) se debe sacar (‘acl’).
Pero hay más cosas que se pueden realizar; cuando se realiza la compilación por parte del compilador (gcc o clang/llvm), se pueden realizar medidas de endurecimiento en seguridad (hardening) como verificar que los punteros no apunten hacia posiciones inválidas, o la creación de un tercer sector en la RAM (adicional al stack y al heap), deshabilitar funciones y estructuras obsoletas en C, entre otras medidas. Así mismo también existe una colección de herramientas para el tratamiento de binarios a posteriori de su compilación; binutils los cuales se involucran en el proceso final de compilación y ensamblaje.
Compilador | Arquitectura | Medida | Propósito |
---|---|---|---|
GCC | Todas | -Wtrampolines | Habilita mensajes de alerta sobre trampolines que requieren stacks ejecutables |
GCC / Clang | Todas | -Wall -Wextra | Habilita mensajes de alerta sobre uso de variables o código que pueden conducir a problemas de seguridad |
GCC / Clang | Todas | -Wformat -Wformat=2 | Habilita mensajes de alerta sobre el uso de ciertos formatos de sintaxis que puede conducir a problemas de seguridad |
GCC / Clang | Todas | -Wconversion -Wsign-conversion | Habilita mensajes de alerta sobre el uso de conversiones en tipos (un char y un int por ejemplo) |
GCC / Clang | Todas | -Wimplicit-fallthrough | Habilita mensajes de alerta cuando hay casos de uso en donde el “switch” puede fallar |
GCC / Clang | Todas | -Werror | Trata todos los mensajes de alerta como fallos y hasta que no se resuelven no prosigue |
GCC / Clang | Todas | -D_FORTIFY_SOURCE=3 | Verifica el uso de funciones de la libreria libc para detectar usos inseguros y de buffer overflow, y trata de corregirlos |
GCC / Clang | Todas | -D_GLIBCXX_ASSERTIONS -D_LIBCPP_ASSERT | Verifica las llamadas a la librería estándar de C++ para detectar usos inseguros y de buffer overflow, y trata de corregirlos |
GCC / Clang | Todas | -fstrict-flex-arrays=3 | Si una estructura de arreglos (arrays) fija está vacía la convierte en una flexible |
GCC / Clang | Todas | -fstack-protector-strong | Habilita verificaciones en tiempo de ejecución (cuando el programa se ejecuta) para detectar buffer overflows en el stack |
GCC / Clang | Todas | fstack-clash-protection | Habilita verificaciones en tiempo de ejecución (cuando el programa se ejecuta) para validar las alocaciones de tamaño variable en el stack |
GCC / Clang | x86 / x86_64 | -fcf-protection=full | Habilita protección de flujo para el ROP (Return Oriented Programming) y el JOP (Jump Oriented Programming) |
GCC / Clang | AArch64 (arm64) | -mbranch-protection=standard | Habilita protección de ramificación (branch) para el ROP (Return Oriented Programming) y el JOP (Jump Oriented Programming) |
GCC / Clang | Todas | -fno-delete-null-pointer-checks | Fuerza la retención en las verificaciones de punteros nulos |
GCC / Clang | Todas | -fno-strict-aliasing | No asume un solapamiento estricto |
GCC / Clang | Todas | -ftrivial-auto-var-init | Realiza inicializaciones “triviales” de variables para evitar valores inválidos |
Binutils (ld) | Todas | -Wl,-z,nodlopen | Restringe el uso de la llamada “dlopen” a shared objects (los archivos .so) |
Bintuils (ld) | Todas | -Wl,-z,noexecstack | Previene la ejecución de datos, marcando las partes de la memoria stack como no-ejecutable |
Binutils (ld) | Todas | -Wl,-z,relro -Wl,-z,now | Marca las tabals de relocación del binario como de solo lectura, evitando así que se pueda redirigir hacia valores infectados |
Con los valores anteriormente mencionados no he tenido problemas para compilar; systemd, librewolf, firefox, nginx, libreoffice y otros. Pero hay dos valores que sí pueden dar problemas (van a darlos casi seguro) y no son de fácil identificación:
Compilador | Arquitectura | Medida | Propósito |
---|---|---|---|
Bintuils / GCC / Clang | Todas | -fPIE -pie | Construye el ejecutable como posición independiente. |
Bintuils / GCC / Clang | Todas | -fPIC -shared | Construye el código como posición independiente. |
Otros compiladores como rustc (el compilador del lenguaje de programación Rust) aplican por defecto las siguientes protecciones;
- Position-independent executable (PIE)
- Integer overflow checks
- Non-executable memory regions (PIC)
- Stack clashing protection
- Read-only relocations y prestamo inmediata
- Protección contra la corrupción del heap de memoria
- Protección contra la imposición del stack de memoria
- Protección de control de flujo (forward y backward)
Sin embargo puedo indicarte una serie de flags que podes usar en rustc;
Compilador | Arquitectura | Medida | Propósito |
---|---|---|---|
Rustc | Todas | “-C”, “link-arg=-static” | Enlaza el binario final como estático contra las librerías compartidas |
Rustc | Todas | “-C”, “target-feature=+crt-static” | Enlaza el binario final como estático contra el sistema operativo y la librería estándar de C |
Rustc | Todas | “-C”, “link-arg=-fuse-ld=mold” | Usa mold como enlazador para aumentar la performance de enlazamiento |
Rustc | Todas | “-C”, “opt-level=2” | Usa el segundo nivel de optimización en el binario final |
Rustc | Todas | “-C”, “strip=debuginfo” “-C”, “strip=symbols” -C“, “debug-assertions=false” | Elimina todos los símbolos de depuración del binario final |
Tanto los valores de GCC, Cland como de binutils y los de rustc deben ser especificados en el archivo de configuración respectivo de tu gestor de paquetes;
Gestor de paquete | Archivo de configuración | Compilador | Lugar de configuración |
---|---|---|---|
pacman | /etc/makepkg.conf | GCC / Clang | CFLAGS |
pacman | /etc/makepkg.conf | GCC / Clang | CXXFLAGS |
pacman | /etc/makepkg.conf | Binutils | LDFLAGS |
pacman | /etc/makepkg.conf | Rustc | RUSTFLAGS |
apk | No tiene, se usan las variables | - | - |
apt | No tiene, se usan las variables | - | - |
dnf | No tiene, se usan las variables | - | - |
Finalmente, si entendiste lo mencionado hasta ahora comprenderás que no solamente es para el set básico de coreutils o BusyBox, esto sirve para cualquier programa y se debe realizar para poder endurecer la seguridad de los programas en el sistema operativo. Creando los archivos respectivos de PKGBUILD o APKBUILD podes automatizar fácilmente esto, de tal manera que solamente cambias la versión y la sumatoria criptográfica hash y generas el nuevo paquete.
Endurecimiento del binario final
Como vimos anteriormente, el endurecimiento de un binario final (que no sea el kernel) consta en; identificar las funcionalidades requeridas, desactivarlas en la configuración y aplicar flags de endurecimiento en el proceso de compilación.
Este es un proceso que debe hacerse en forma tranquila y con paciencia, en mi experiencia (manteniendo paquetes para mi repositorio de Arch Linux) puede llegar a ser muy frustante cuando la compilación falla por determinadas llamadas o funciones en librerias arbitrarias (azar) o que el programa falla por determinados motivos.
Vamos a utilizar como ejemplo; LibreOffice, Firefox y Nginx.
Proceso;
a. Identificación y mitigación
Primero debemos identificar que grupo de usuarios es nuevo objetivo; ¿es público técnico o no? No es lo mismo el nivel de detalle ante un fallo que nos puede dar el equipo de administradores de los sistemas de la organización, que el departamento administrativo.
¿Qué funciones necesitamos? Cada grupo de usuarios tiene requerimientos de funcionalidades diferentes; algunos van a requerir LibreOffice ONE para poder automatizar cosas mediante scripts (javascript o python), mientras que otros solamente van a necesitar las funciones básicas de editar el documento per-se para poder visualizarlo.
Con nginx algunos van a necesitar solamente el mostrar páginas web planas (sin javascript), mientras que el equipo de ingeniería va a requerir utilizar sus capacidades de proxy reverso para ocultar la infraestructura interna de la compañia (módulo; rewrite).
Con Firefox algunos van a necesitar las funcionalidades básicas, sin WebRTC (ya que no usan Google Meet, Microsoft Teams, etc) ó el soporte de DRM (Netflix, Disney+, etc) o el soporte de sincronización entre dispositivos, mientras que otros usuarios sí. Considerar que en el navegador web interviene mucho los gustos personales, por lo que entramos en un terreno de no objetividad por parte de un usuario/a.
Entonces, ¿cómo identificar correctamente los requerimientos? Mediante encuentras, entrevistas y…. observando como escuchando. Jamás subestime la capacidad de aprender del comportamiento de las personas cuando se las puede observar y escuchar sin saber que están siendo analizadas. Se conoce como; “Efecto Hawthorne” al cambio en el comportamiento cuando una persona sabe que está siendo observada. La información de observar y anotar el patrón de uso de una determinada herramienta le puede indicar cosas muy valiosas como; si la herramienta se utiliza solamente en momentos críticos donde la persona se encuentra nerviosa y necesita que la apertura sea tan rápida como sea posible o bajo que condiciones (importante si tenemos en consideración que el hardening en un binario tiene una pequeña penalización de performance).
Una vez analizado e identificado el patŕon de uso y requerimiento en el grupo objetivo debemos analizar el código fuente del programa, no se trata de analizar el código fuente en el lenguaje de programación que esté hecho (que es algo que debería hacerse si es un programa open-source) si no de verificar en el proceso de configuración que opciones permite deshabilitar.
Hay un estándar de facto para los programas creados en C y C++; el utilizar scripts de configuración que cambien valores en el archivo de construcción (que será leído por el programa; “make”) para habilitar o no ciertas características (del inglés; “features”).
Código fuente de LibreWolf 125.0.2 |
---|
Nótese el archivo “configure” |
Si el script respeta el estándar se le puede pasar el argumento; “--help” para obtener los detalles de configuración;
Algunas de las opciones de configuración de LibreWolf 125.0.2 |
---|
Por lo tanto, usted debe armarse de paciencia y verificar que las funcionalidades no requeridas por el grupo objetivo puedan ser deshabilitadas.
Nota técnica |
---|
LibreOffice suele variar mucho entre diferentes versiones que se requiere sí o sí y que se puede deshabilitar, en la versión 24.2.2.2 no pude hacerlo funcionar en la compilación sin tener Java habilitado expresamente (por el wrapper entre C++ y Java), a pesar de que a nivel de funcionamiento es un opcional. |
Por último, y muy muy muy importante, debemos identificar que dependencias usa la aplicación para funcionar.
En los sistemas Linux y BSD tenemos el “enlazamiento dinámico” que es la capacidad de usar funcionalidades de otras aplicaciones instaladas para su uso interno. Mediante este tipo de funcionalidad externa se provee mediante un archivo llamado “shared object” (extensión; “.so”) el cual otro programa puede utilizar para hacer ciertas acciones.
Como indiqué en “Arquitectura” -> “Organización”, la ubicación estándar para las librerías/objetos compartidos es; “”/usr/lib“
También la aplicación puede utilizar “enlazamiento estático”, el cual el programa enlazador (llamado; ld) agarra la librería/objeto compartido y lo embebe directo en el programa.
Ventajas del enlazamiento dinámico;
- Ahorra espacio, ya que muchas aplicaciones pueden usar el mismo shared object.
- Ahorra memoria, ya que cuando el shared object se carga en memoria, el resto de programas usan esa instancia.
- Al actualizar el programa que lo usa, las aplicaciones dependiente de ese shared object reciben sus ventajas con la nueva actualización.
Desventajas del enlazamiento dinámico;
- Si hay una vulnerabilidad en el programa que provee el shared object, entonces todos los programas que usen tal dependencia son afectados.
- Si el programa que provee el shared object se actualiza y rompe retro compatibilidad, entonces todos los programas que usen tal dependencia son afectados.
- Puede ocasionar en el sistema un enorme lío de programas y dependencias, y que los dos problemas anteriores se agraven.
Ventajas del enlazamiento dinámico;
- El binario se vuelve auto suficiente, solamente necesita el kernel del sistema operativo.
- El programa funciona más rápido al no tener que estar cargando rangos de memoria o archivos específicos.
- Si un programa tiene una versión específica de la librería/objeto embebido, es independiente de actualizaciones en otros.
- Cada programa al usar su propia instancia, se evitan problemas de seguridad asociados.
- MUSL (la librería estándar específica para Linux) suele ser más rápida, liviana y minimalista que GLibC.
Desventajas del enlazamiento dinámico;
- A veces es imposible enlazar estáticamente un programa; Firefox, LibreWolf, Chromium y otros son tan complejos que es imposible. Otros más simples puede ser más fácil.
Firefox enlazado dinámicamente incluso en Alpine, que trata de enlazar todo estáticamente |
---|
- Se usa más memoria RAM, ya que cada programa usa su propia instancia de sus dependencias.
- Si la dependencia embebida (por el enlazamiento estático) tiene una vulnerabilidad, hasta que no se actualice no se solucionará. Con lo que en software abandonado y/o desactualizado es un grave problema.
Se puede verificar directamente si la aplicación usa “enlazamiento dinámico”;
ldd [path_absoluto_al_finario]
Si continuamos con el ejemplo de Firefox en Alpine de la imagen superior, podemos identificar que tiene dependencias operativas de las librerías compartidas;
- ld-musl-x86_64
- libscudo
- libstdc++
- libgcc_s
- libc.musl-x86_64
Por lo que cuando compilemos por nuestra cuenta Firefox debemos tener en consideración dos opciones;
- Son dependencias absolutas e indispensables, y deben satisfacerse sí o sí.
- Son dependencias provenientes de determinadas características opcionales, si las deshabilitamos podemos prescindir de ellas, si las necesitamos y no podemos compilar estáticamente el programa entonces necesitaremos el programa que provee tal shared object.
b. Endurecimiento
Dependiendo del lenguaje de programación utilizado en el programa que estamos compilando, los flags de endurecimiento no necesariamente deben pasarse de forma manual en cada llamada al compilador. Se puede automatizar que argumentos/flags usarán, el lugar de especificación variará;
Lenguaje | Lugar de especificación |
---|---|
C y C++ | Variables de entorno; “CFLAGS” y “CXXFLAGS” antes de llamar al script “configure”, o en “/etc/makepkg.conf” para sistemas Arch Linux y derivados |
Go | En la ejecución del compilador “go”, especificando algún flag del listado disponible |
Rust | En el archivo de configuración; siendo el global “~/.cargo/config.toml” o en la carpeta del proyecto “.cargo/config.toml”, en ambos es dentro de la variable “rustflags”. Si es el compilador “rustc” directamente, se añaden como argumentos (recomiendo especificar una variable de entorno que los contenga) |
Proceso final
Hasta vimos una teoría de todo lo que debe hacerse, ahora vamos a la práctica.
Nota |
---|
Como comenté antes, acá estaré explicando para el gestor de paquetes “pacman” pero usted puede adaptar esto a apt, dnf, etc |
Vamos a crear una nueva carpeta nueva y dentro un archivo llamado “PKGBUILD”. Al igual que con el ejemplo de los “coreutils”, el archivo indicará las funciones elementales que el script “makepkg” ejecutará para crear el binario final (utilizando bash como dependencia).
Podemos utilizar el siguiente template. Lo que se debe remplazar es lo indicado entre corchetes;
pkgbase=[nombre_del_paquete_que_estamos_creando]
pkgname=('[idem_pkgbase]')
pkgver=[versión_del_paquete]
pkgdesc="[Descripcion_del_paquete_para_cuando_se_hace_una_busqueda]"
pkgrel=[numero_de_liberacion_empezando_desde_1]
arch=('[arquitectura_que_soporta_1]', '[arquitectura_que_soporta_N]')
license=('[licencia_del_codigo_fuente]')
url="[url_del_codigo_fuente_del_proyecto]"
depends=('[dependencia_para_funcionar-1]' '[dependencia_para_funcionar-N]')
makedepens=('[dependencia_para_funcionar-1]' '[dependencia_para_funcionar-N]')
source=("[url_del_código_fuente_a_descargar]")
validgpgkeys=('[firma_pgp_del_codigo_fuente]')
sha256sums=(
'[sumatoria_hash_sha256-archivo-1]'
'[sumatoria_hash_sha256-archivo-2]'
'[sumatoria_hash_sha256-archivo-N]'
)
prepare(){
[acciones_a_realizar_antes_de_proceder_a_compilar]
}
build(){
[acciones_para_compilar_incluido_lo_que_queres_y_no_queres]
}
check(){
[opcional_pero_util]
}
package(){
[opciones_personalizadas]
make DESTDIR="$pkgdir" install
}
En la variable “source” podemos usar las variables anteriormente declaras ($pkgbase, $pkgver) para automatizar que busque la última versión.
La variable “pkgdir” es una carpeta temporal que “pacman” usa para identificar todos los archivos y carpetas que crea el progrma compilado.
Podes verificar mi repositorio para ver como modifico LibreWolf, Firefox, LibreOffice, SystemD, Nginx y otros para minimalizarlos de esta forma;
- https://github.com/ShyanJMC/minimal_packages
Una vez que tenemos nuestro archivo de construcción (uno por carpeta como mínimo y máximo) ejecutamos;
makepkg -s
El argumento “-s” indica de instalar las dependencias para construirlo y para funcionar. Una vez que finalice el proceso en la misma carpeta aparecerá un archivo con la extensión; “.pkg.tar.zst” (package tar zstandard) que luego se puede añadir al repositorio o instalar manualmente con “pacman -U [archivo]”.
Más adelante veremos como hacer este proceso de una manera más limpia apropiadamente mediante contenedores para no dejar el sistema principal lleno de programas.
Endurecimiento del kernel
Entramos en una sección complicada; como el kernel es el programa bare metal que controla todo el resto del sistema, hacer malas hardenizaciones puede conducir a que no quede del todo operativo o estable.
Como mencioné anteriormente, no debería haber problema en usar los flags de hardenización de GCC, Clang, Rustc y Binutils siempre que no sean PIE y PIC.
El código fuente del kernel viene por defecto con unos valores por defecto para las variables; CFLAGS, CXXFLAGS y RUSTFLAGS
Valores por defecto |
---|
Nota; la utilización de “-Z xxxx” en rustc indica que es el toolchain inestable (llamado; nightly).
El hardening del kernel viene, principalmente, en dos vertientes;
-
Disminución de la superficie de ataque
A mayor cantidad de características (“features”) mayor posibilidad de encontrar una vulnerabilidad de seguridad en alguna de ellas.
Y Linux a pesar de ser el kernel de mayor evolución en la historia, no es invulnerable;
- https://www.cvedetails.com/vulnerability-list/vendor_id-33/product_id-47/Linux-Linux-Kernel.html
Por lo tanto, debemos compilar nuestro propio kernel y eliminar del binario final todo lo que no utilicemos en nuestro dispositivo.
Para hacerlo, debemos descargar el código fuente estable desde kernel.org, descomprimirlo y ejecutar;
make menuconfig
Una vez abierto el menu de configuración;
-
Nos movemos con las flechas; ↑ y ↓
-
Cambiamos entre las opciones “Select”, “Exit” y “Help” con; ← y →
Si no sabes que hace una determinada opción o configuración, el ir a “Help” te dará detalles técnicos sobre que se trata.
-
Seleccionamos una opción con la tecla de espacio / “space”
-
Las opciones marcadas con “*” formarán parte embebida del núcleo sí o sí. A esto me referiré como “embebida”.
-
Las opciones marcadas con “M” serán compiladas como módulos. A esto me referirié como “módulos”.
En las diferentes secciones que nos ofrece nos encontraremos con diversas características configurables del kernel. Para saber como configurar el kernel debemos tener en consideración el hardware que debe soportar el núcleo que estamos configurando;
-
Procesador y arquitectura que estamos usando; ARM, Intel, AMD, PowerPC, etc
Para obtener esta información podemos verificar el archivo; “/proc/cpuinfo”
-
Almacenamiento en donde se encontrará el núcleo y el resto del sistema; SSD, HDD, Pendrive por USB, etc
Para obtener esta información podemos ejecutar;
lsblk -f -o name,size,fstype,label,model,serial,mountpoint
Debemos prestar atención a “FSTYPE” (que indica el sistema de archivos), su punto de montaje (“mountpoint”) y el “NAME” del dispositivo;
- Si el dispositivo es; sdaX, sdbX, etc (siendo “X” un número) entonces el dispositivo es un dispositivo SATA
- Si el dispositivo es; nvme0n1, nvme1n1, etc entonces el dispositivo es un SSD M.2
- Si el dispositivo es; hdX (siendo “X” un número) entonces el dispositivo es un IDE
-
Sistema de archivos que usaremos tanto en “/boot” como en el “/” del sistema (y /home si está en partición separada); EXT4, BTRFS, XFS, etc
-
Dispositivos que tenemos conectados mediante PCI y PCI-Express
Para obtener esta información podemos ejecutar;
lspci -k
El comando mostrará todos los dispositivos conectados por PCI y PCI-Express al sistema, y argumento “-k” mostrará que módulo/driver del núcleo se utiliza para manejar el mismo. Esto es de especial importancia para asegurarte que estás configurando correctamente los dispositivos.
-
Método de booteo
Si estamos usando una single board computer (como es recomendación por su bajo consumo energético y portabilidad) debemos considerar que cada una tiene una forma de booteo diferente, que está determinada por el firmware del CPU.
Por ejemplo, en la raspberry pi 3b+ (ARMv8) si usamos el kernel puro de forma directa en el booteo el mismo deberá realizar una serie de pasos previos para iniciar tanto los núcleos como la GPU del SOC, lo cual te puedo asegurar que casi no lograrás que inicie. Para suplir esto se utiliza como iniciador subyacente y como cargador de arranque a; U-Boot
Una vez que tenemos la información básica sobre nuestro hardware podemos empezar a configurar el kernel. Las configuraciones se distribuyen entre estos grandes grupos;
-
General Setup
Tenemos las partes bases del núcleo para lo que es el procesador y la memoria RAM. Así como la compatibilidad con el sistema POSIX.
Recomiendo revisar y dejar;
-
“Core scheduling for SMT”
Esto ayudará a que en procesadores de muchos núcleos se pueda ejecutar las tareas multi núcleo de forma más eficiente.
-
“CPU isolation”
Esto lo que hace es que los núcleos del CPU que estén ejecutando tareas críticas los aisla para que no se interrumpa su trabajo (ni siquiera con los IRQ).
Se debe tener en consideración que ni siquiera el “scheduler” del núcleo (que se encarga de determinar cuanto tiempo y en que núcleo del CPU se ejecuta un proceso), modificará o se meterá con lo que se esté ejecutando. Esta es una opción que puede resultar muy buena para procesos que requieren una latencia casi in-extremis de baja.
Su utilización requiere del parámatro de booteo; “isolcpus=[CPU_CORE_NUMBER]”
-
Las configuraciones específicas para tu procesador
Deben ser compilados como embebidos.
-
El soporte para que el kernel pueda exponer su configuración en; “/proc/config.gz”
Esto permitirá que puedas copiar ese archivo entre versiones del kernel para poder actualizaciones automatizadas. Así mismo seguramente te diste cuenta que tiene la extensión “gz” (la versión zip de GNU) por lo que no te olvides de marcar el soporte para ese protocolo de compresión (GNU ZIP).
-
Acá también se encuentra la opción de poder usar el initramfs (archivo que se usa en RAM para cargar los módulos) pero si compilas como embebidos el soporte de almacenamiento usado y del sistema de archivos (y de criptografía si usas el “/” cifrado) no es necesario.
-
Todos los “Control Group” como embebidos
Esto es un requisito para tener soporte para contenedores.
Y si te suena “cgroups” es exactamente esto; la capacidad de controlar la forma en que determinados programas acceder al procesador y la RAM, pudiendo desde crear cuotas para limitar el consumo de recursos, hasta aislamientos por proceso o grupo de procesos (los contenedores).
-
-
Processor type and features
Tenenos todas las características no escenciales pero que dan mayores funcionalidades a los procesadores generales.
Recomiendo revisar y dejar;
-
“Machine Check / overhating reportting”
Esto permitirá utilizar el procesador para verificar los sensores de temperatura que tiene embebidos. Deja solamente el de AMD ó Intel según el procesador que vos uses.
-
“AMD Secure Memory Encryptioin (SME) support”
En caso de los procesadores AMD, permite utilizar una llave generada al momento de la carga del firmware para cifrar de forma transparente las tablas y páginas de la memoria RAM en conjunto con el núcleo.
-
“Check for low memory corruption”
En casos de corrupción de memoria RAM, que se sospecha que puede ser por la BIOS/UEFI, el sistema puede colgarse completamente.
Para evitar usar esos sectores corruptos, el kernel escanea 64kb cada 60 segundos y si detecta corrupción evita utilizar esos sectores.
Para ser utilizado no solamente debe habilitarse en la compilación esto, si no que en los argumentos de inicio el kernel debe tener; “memory_corruption_check=1”
-
“Memory Protection Keys”
Provee un mecanismo para forzar protecciones de muy bajo nivel a las páginas de memoria RAM, sin la necesidad de modificar las tablas (que contienen varias páginas) previamente existentes.
Es específico de los procesadores Intel.
-
“Randomize the address of the kernel image”
Aleatoriza las direcciones físicas en donde la imagen del núcleo se descomprime y donde es posteriormente mapeado la misma.
En sistemas con RDRAND y RDTSC soportados se los puede usar para generar entropía y mejorar la seguridad de esta característica. RDRAND es una característica que permite al CPU generar números pseudo aleatorios desde una fuente de entropía.
Es específico de los procesadores Intel.
-
“Randomize the kernel memory sections”
Aleatoria las secciones físicas y virtuales que son utilizadas por la memoria del núcleo.
-
“Disable the 32-bit vDSO (needed for glibc 2.3.3)”
Existe una vulnerabilidad conocida en glibc 2.3.3 (2004-2005), que afecta a los progrmas causando errores inesperados en el manejo de memoria y prohibiendo diversas causas de congelamiento del sistema.
Tengamos en cuenta que estamos hablando de un versionado de software de hace ya 20 años (ya que esto lo estoy escribiendo en el 2024), pero puede pasar que por causas fortuitas se esté usando un software compilado de esa época en algún contenedor de docker/podman/cri-o y se necesite aplicar ciertas medida de seguridad.
Por lo que esto se debe considerar si el software compilado en aquel momento está usando esa versión de glibc.
-
“Built-in kernel command line”
Los argumentos que se le indican al núcleo (desde GRUB, LILO o el bootloader que sea) se conocen como CMDLINE.
Esta funcionalidad permite que el kernel tenga un CMDLINE embebido interno. Esto es un arma de doble filo, por un lado permite que ciertas características (como características de seguridad a través del argumento lsm=xxxxx, el nivel de log, etc) no se deshbiliten pero si las mismas deben cambiar por algún motivo entonces toca recompilar el kernel.
Mi recomendación; si tu kernel está en el mismo dispositivo de almacenamiento que la partición root del sistema (“/”), poné “root=UUID=XXXXXXXXXX” y el “cryptdevice=UUID=XXXXXXXXXX”. El argumento “cryptdevice” indica el ID de la partición root del sistema cifrada, y el argumento “root” indica el ID de la partición root del sistema luego de ser descfriada. Esto no lo veo como una desventaja, si cambias de dispositivo SSD lo normal es que actualices el kernel y recompiles, admeás de que el ID de la misma cambiará.
-
-
“Mitigations for speculative execution vulnerabilities”
Esto viene a raiz de las vulnerabilidades enconradas en procesadores ARM, x86 Intel, x86 AMD, PowerPC;
- Meltdown (CVE-2017-5754) y Spectre (CVE-2017-5753 y CVE-2017-5715).
Explicaciones directa desde Wikipedia;
Meltdown es un agujero de seguridad en el hardware que afecta a los procesadores Intel x86, a los procesadores IBM POWER, y también a algunos procesadores basados en la arquitectura ARM, y que permite que un proceso malicioso pueda leer de cualquier lugar de la memoria virtual, aún sin contar con autorización para hacerlo. Meltdown afecta a una amplia gama de sistemas. En el momento de hacerse pública su existencia, se incluían todos los dispositivos que no utilizasen una versión convenientemente parcheada de iOS, GNU/Linux, MacOS, Windows y Windows 10 Mobile. Por lo tanto, muchos servidores y servicios en la nube se han visto impactados, así como potencialmente la mayoría de dispositivos inteligentes y sistemas embebidos que utilizan procesadores con arquitectura ARM (dispositivos móviles, televisores inteligentes y otros), incluyendo una amplia gama de equipo usado en redes. Se ha considerado que una solución basada únicamente en software para Meltdown ralentizaría los ordenadores entre un cinco y un 30 por ciento dependiendo de la tarea que realizasen. Por su parte, las compañías responsables de la corrección del software en relación con este agujero de seguridad informan de un impacto mínimo según pruebas tipo
Spectre es una vulnerabilidad que afecta a los microprocesadores modernos que utilizan predicción de saltos. En la mayoría de los procesadores, la ejecución especulativa que surge de un fallo de la predicción puede dejar efectos observables colaterales que pueden revelar información privada a un atacante. Por ejemplo, si el patrón de accesos a la memoria realizados por la mencionada ejecución especulativa depende de datos privados, el estado resultante de la caché de datos constituye un canal lateral mediante el cual un atacante puede ser capaz de obtener información acerca de los datos privados empleando un ataque sincronizado.
Por ende, todo sistema de mitigación que se le pueda dar a esto es poco.
Dejaremos habilitado;
-
“Remove the kernel mapping in user mode”
Esto reduce mucho de los saltos laterales posibles al evitar que la memoria del núcleo no esté mapeado en el espacio de usuario (‘userspace’).
-
“Avoid speculative indirect branches in kernel”
Esta configuración utiliza el argumento “-mindirect-branch=thunk-extern” del compilador utilizado para hacer que en caso de realizarse un salto a un área protegida del núcleo se devuelva una denegación o que se evite tal salto lateral desde el CPU.
-
“Enable return-thunks”
Refuerza a la opción “Avoid speculative indirect branches in kernel”.
-
“Enable UNRET on kernel entry”
UNRET = unreturn
Refuerza a la opción “Avoid speculative indirect branches in kernel”.
-
“Mitigate RSB underflow with call depth tracking”
Los Intel SKL Return-Speculation-Buffer (RSB) tiene un problema de seguridad cuando ocurren errores de underflow (lo opuesto al overflow, que es cuando se intenta representar valores más chicos del permitido).
Esto necesita el argumento “retbleed=stuff” en el CMDLINE (los argumentos que se le pasan al kernel al iniciarlo).
-
“Enable IBPB on kernel entry”
Retbleed es una variante del ataque por especulación Spectre.
El modo IBPB (Indirect Branch Prediction Barrier) es una medida para mitigar a modos en que pueden ejecutarse Retbleed / Spectre.
Esto necesita el argumento “retbleed=ibpb” en el CMDLINE (los argumentos que se le pasan al kernel al iniciarlo).
-
“Enable IBRS on kernel entry”
Compila el kernel con soporte para mitigar Spectrev2 y Retbleed.
Produce una péridida significativa de rendimiento.
Esto necesita el argumento “spectre_v2=ibrs” en el CMDLINE (los argumentos que se le pasan al kernel al iniciarlo).
-
“Mitigate speculative RAS overflow on AMD”
RAS = “Return Stack Overflow”
Es específico de los procesadores AMD.
Esta medida es una mitigación para evitar otro tipo de ataque de especulación ejecutiva en procesaodres AMD (generación Zen 1era a la 4rta).
-
“Mitigate Straight-Line-Speculation”
Compila el kernel con opciones de especulación de línea recta para protegerse de la especulación de línea recta. La imagen del núcleo puede ser ligeramente mayor.
-
“Force GDS Mitigation”
Es específico de los procesadores Intel.
La vulnerabilidad Gather Data Sampling (GDS) afecta directamente al hardware. Tiene un objetivo de explotación de la misma naturaleza que Meltdown y Spectre, pero apuntando directamente a las instrucciones AVX2 y AVX-512.
-
“RFDS Mitigation”
Es específico de los procesadores Intel Atom.
De la misma naturaleza que GDS, Meltdown y Spectre, al atacar ejecuciones especulativas.
-
“Mitigate Spectre-BHB (Branch History Injection)”
Spectre-BHB es la versión 2 de Spectre, esta es una medida de mitigación.
-
“Enable loadable module support”
Dejaremos habilitado;
-
“Module signature verification”
Es requerimiento para el módulo de seguridad Lockdown. Funcionará si el binario final no se le pasa un strip.
Añade como dependencia a OpenSSL para verificar la firma criptográfica que está adjunta al módulo.
-
-
“Memory Management Options”
Dejaremos habilitado;
-
“Page allocator randomization”
Reduce la predictibilidad de las alocaciones de las páginas en la memoria virtual usando la especificación ACPI 6.2a y en específico; “Heterogeneous Memory Attribute Table (HMAT)”.
-
-
“Security Options”
Bueno, acá uno pensaría; “Shyan, ¿habilitamos todo no?”, no. No es inteligente habilitar por habilitar.
No habilitaremos los módulos simplemente por habilitar, en base a nuestras necesidades y conocimientos habilitaremos unos u otros.
Yo recomiendo, para usuarios domésticos, dejar; Lockdown, AppArmor, “Integrity Subsystem” y “Simplified Mandatory Access Control Kernel Support”.
Luego dejaremos habilitado;
-
“Enable temporary caching of the last request_key() result”
Esta opción hace que el resultado de la última llamada exitosa a request_key() que no realizó una llamada al kernel se almacene en caché temporalmente en task_struct. La caché se borra al salir y justo antes de reanudar el espacio de usuario.
Esto permite que la clave utilizada para procesos de múltiples pasos donde cada paso desea solicitar una clave que probablemente sea la misma que la solicitada en el último paso se guarde en la búsqueda.
Un ejemplo de tal proceso es un recorrido a través de un sistema de archivos de red en el que cada método necesita solicitar una clave de autenticación. Pathwalk llamará a múltiples métodos para cada dentry atravesado (permission, d_revalidate, lookup, getxattr, getacl, …).
-
“Enable register of persistent per-UID keyrings”
Esta opción proporciona un registro de conjuntos de claves persistentes por UID, dirigido principalmente al almacenamiento de claves Kerberos. Los llaveros son persistentes en el sentido de que permanecen después de que todos los procesos de ese UID hayan finalizado, no que sobrevivan al reinicio de la máquina.
Un conjunto de claves en particular puede ser accedido por el usuario cuyo conjunto de claves es o por un proceso con privilegios administrativos. Los LSM activos deciden qué procesos de nivel de administrador pueden acceder al caché.
Los llaveros se crean y agregan al registro a pedido y se eliminan si caducan (se establece un tiempo de espera predeterminado al momento de la creación).
-
“TRUSTED KEYS”
Esta opción brinda soporte para crear, sellar y desbloquear claves en el kernel. Las claves confiables son claves simétricas de números aleatorios, generadas y selladas por una fuente confiable seleccionada en el momento del arranque del kernel.
El espacio de usuario solo verá blobs cifrados.
-
“ENCRYPTED KEYS”
Esta opción proporciona soporte para crear/cifrar/descifrar claves en el kernel.
Las claves cifradas se crean instancias utilizando números aleatorios generados por el kernel o datos descifrados proporcionados, y se cifran/descifran con una clave simétrica “maestra”.
La clave ‘maestra’ puede ser del tipo clave de confianza o de usuario. Solo los blobs cifrados se envían al espacio de usuario.
-
“Enable different security models”
Esta opción es lo que permite usar LSM más allá de; “Enable the securityfs filesystem”.
-
“Socket and Networking Security Hooks”
Esto habilita los hooks de seguridad del socket y de la red. Si está habilitado, un módulo de seguridad puede usar estos hooks para implementar controles de acceso a redes y sockets.
-
“XFRM (IPSec) Networking Security Hooks”
Esto habilita los hooks de seguridad de red XFRM (IPSec). Si está habilitado, un módulo de seguridad puede usar estos enlaces para implementar controles de acceso por paquete basados en etiquetas derivadas de la política IPSec.
Las comunicaciones que no son IPSec se designan como sin etiquetar y solo los sockets autorizados para comunicar datos sin etiquetar pueden enviar sin utilizar IPSec.
-
“Security hooks for pathname based access control”
Esto es requerimiento para AppArmor y TOMOYO.
Esto habilita los hooks de seguridad para el control de acceso basado en nombres de rutas. Si está habilitado, un módulo de seguridad puede usar estos enlaces para implementar controles de acceso basados en nombres de rutas.
-
“Harden memory copies between kernel and userspace”
Esta opción busca regiones de memoria obviamente incorrectas al copiar memoria hacia/desde el kernel (a través de las funciones copy_to_user() y copy_from_user()) rechazando rangos de memoria que son mayores que el objeto de montón especificado, que abarcan varias páginas asignadas por separado, que no están en el pila de procesos, o son parte del texto del kernel. Esto evita clases enteras de vulnerabilidades de desbordamiento del montón y exposiciones similares a la memoria del kernel.
-
“Harden common str/mem functions against buffer overflows”
Detecta desbordamientos de búfer en funciones de memoria y cadenas comunes donde el compilador puede determinar y validar los tamaños de búfer.
-
“Force all usermode helper calls through a single binary”
De forma predeterminada, el kernel puede llamar a muchos programas binarios de espacio de usuario diferentes a través de la interfaz del kernel “ayudante de modo de usuario”.
lgunos de estos binarios están definidos estáticamente en el propio código del kernel o como una opción de configuración del kernel. Sin embargo, algunos de ellos se crean dinámicamente en tiempo de ejecución o se pueden modificar después de que se haya iniciado el kernel.
Para proporcionar una capa adicional de seguridad, enrute todas estas llamadas a través de un único ejecutable al que no se le puede cambiar el nombre.
Tenga en cuenta que depende de este único binario llamar al binario auxiliar del modo de usuario “real” correspondiente, en función del primer argumento que se le pasa. Si lo desea, este programa puede filtrar y elegir cómo se llaman los programas reales.
Si desea que todos los programas auxiliares del modo de usuario estén deshabilitados, elija esta opción y luego configure STATIC_USERMODEHELPER_PATH en una cadena vacía.
-
“Kernel hardening options” -> “Memory initialization” -> “Initialize kernel stack variables at function entry (zero-init everything (strongest and safest)”
-
“Kernel hardening options” -> “Memory initialization” -> “Poison kernel stack before returning from syscalls”
-
“Kernel hardening options” -> “Memory initialization” -> “Enable heap memory zeroing on allocation by default
-
“Kernel hardening options” -> “Memory initialization” -> “Enable heap memory zeroing on free by default”
-
“Kernel hardening options” -> “Memory initialization” -> “Enable register zeroing on function exit”
-
“Kernel hardening options” -> “Hardening of kernel data structures” -> “Check integrity of linked list manipulation”
-
“Kernel hardening options” -> “Hardening of kernel data structures” -> “Trigger a BUG when data corruption is detected”
-
“Kernel hardening options” -> “Randomize layout of sensitive kernel structures (Fully randomize structure layout)”
-
-
“Cryptographic API”
Acá sí habilitaremos todo lo que no sean algoritmos criptográficos inseguros como; SHA-1, todos los “testing” ó “debug”, MD4 y MD5.
Una vez que ya tenemos toda la configuración mínima y de seguridad les recomiendo revisar en; “Device Drivers” los componentes que se utilizan sí o sí en sus máquinas, para dejarlos como embebidos, y el resto que no son utilizados de forma constante si no ocasional ahí compilarlos como M; módulos.
Una vez finalizado el proceso de configuración, lo guardamos usando la opción de “Save” en el archivo “.config” y luego compilamos tanto el kernel, como sus módulos y luego los instalamos;
make -j$(nproc) && make install && make modules_install
Luego deberá actualizar la configuración de su bootloader. En el caso de que usemos GRUB2 es con;
grub-mkconfig -o /boot/grub/grub.cfg
Contenedores
En el kernel Linux tenemos una característica (“feature”) llamada; ‘cgroups’ (del acrónimo; “control groups”) el cual le permite al kernel limitar recursos a un proceso (CPU, RAM, almacenamiento, etc) o grupo de tales.
Así mismo, y como el kernel es el encargado de ejecutar y controlar todo, también puede utilizarse cgroups para limitar o relimitar que procesos tienen más prioridad en un CPU.
Esta característica, a partir de la V2 desarrollada desde el 2013, permite una nueva importante; los ‘namespaces’. Los espacios de nombres proveen, además, la capacidad de que un grupo de procesos no puedan ver ciertas partes del sistema.
Todo en su conjunto permite los llamados; contenedores.
Copyright; Pexels.com |
A un nivel más alto y no tan bajo, los contenedores usan las capacidades del kernel para crear un entorno aislado personalizable.
Podemos definir tres tipos de aislamientos básicos;
-
Aislamiento de almacenamiento
El aislamiento de almacenamiento permite que la aplicación y el entorno aislado no puedan ver lo que existe fuera del mismo.
-
Aislamiento de memoria RAM
El aislamiento de memoria RAM permite que la aplicación y el entorno aislado no puedan obtener información fuera del aislamiento donde se ejecutan.
Esto hace que no puedan detectar; programas en funcionamiento, tipo de sistema operativo, hardware donde se ejecuta, entre otros.
-
Aislamiento de red
El aislamiento de red permite que el entorno aislado (con la aplicación) tenga su propio punto de entrada/salida de red, separando lógicamente así los procesos. De tal manera que no pueden analizar el tráfico de otras aplicaciones ni del sistema host.
El punto de entrada/salida de red del contenedor es; “interfaz de red virtual”.
Diferencias
Quizás hallas escuchado hablar de los ‘sandbox’ y la virtualización, para que no se confundan los tres términos (por que sí, son diferentes) abordaremos las diferencias;
-
Un contenedor se diferencia de un sandbox en que el primero (el contenedor) tiene todo el entorno necesario para simular una instalación completa sin serla realmente.
-
Un contenedor se diferencia de una máquina virtual en que el primero (el contenedor) no emula hardware ni inicia otro S.O. si no que la aplicación y el entorno aislado deben poder ejecutarse en el mismo kernel que el sistema host.
Nota |
---|
Quizás vean por internet que a la ‘contenerización’ también se le llama “virtualización a nivel de sistema operativo”. |
- Mientras los sandbox no usan un entorno y la virtualización usa un sistema operativo completo, los contenedores usan las “imágenes” como archivos comprimidos (extensión; .tar.gz) para el entorno a usar.
Estándar
Debido a que el contenedor tiene un entorno completo y minimalista para que la aplicación (o el conjunto de tales) aislada se comporte como si estuviera en un sistema completo, pueden haber muchas diferencias a la hora de implementar tales medidas y su nivel de profundización.
Para solucionar tal problema, existe la OCI (Open container Initiative). La OCI es un proyecto amparado por la Linux Foundation para diseñar, planificar, desplegar y publicar un estándar abierto de compatibilidad.
Por lo tanto, todo motor (‘engine’) que soporte el estándar OCI va a ser compatible con el resto.
Engines compatibles con el estándar OCI
En Linux tenemos varios motores de contenedores (ó “virtualización a nivel de sistema operativo”) que son compatibles con el estándar OCI;
-
Docker
https://www.docker.com/
A contrario de lo que la mayoría piensa; docker no es un engine de contenedores. En realidad el proyecto que es el motor per-se es Moby (‘Docker’ es la empresa);
URL; https://www.docker.com/blog/introducing-the-moby-project/ Moby fue donado a la OCI hace un par de años.
Moby no es más que un frontend para el verdadero engine; containerd.
-
Podman
https://podman.io/
Podman es una librería y programa creado originalmente por Red Hat para suplir una deficiencia de docker/containerd; debe ejecutarse como root.
Podman tiene la ventaja de no necesitar ejecutarse como root, los contenedores se ejecutan con los privilegios del usuario que los crea. Reduciendo así el nivel de exposición a ataques y elevaciones de privilegios. Debido a esto se debe modficar los archivos; “/etc/subgid” y “/etc/subuid” para permitir un aumento en los archivos y descriptores abiertos por parte del usuario;
[usuario]:100000:65536
-
CRI-O
CRI-O es un engine diseñado en exclusiva para funcionar en Kubernetes. Al igual que docker necesita de permisos root para poder ejecutarse.
El comando de CRI-O es; crictl.
Comandos OCI
Algunos comandos básicos:
- Descargar imagen desde un repositorio X;
[docker/podman/crictl] pull [repository]/[imagen]:[tag]
- Iniciar un contenedor
[docker/podman/crictl] run [opciones] [imagen]:[tag] [PID1]
-
Si se quiere usar el contenedor de forma interactiva se debe poner la opción; “-ti” (t: TTY e i: interactive).
-
Si se quiere poner un hostname se debe poner la opción; “-h [nombre]”.
-
Si se quiere poner un nombre específico al contenedor se debe poner la opción; “–name [nombre]”.
- Listar contenedores en funcionamiento
[docker/podman/crictl] ps
- Listar todos los contenedores
[docker/podman/crictl] ps -a
- Inspeccionar un contenedor
[docker/podman/crictl] inspect [container_ID]
- Copiar archivos desde un contenedor
[docker/podman/crictl] cp [container_ID]:[PATH_abosluto] [destino_PATH_absoluto]
- Copiar archivos hacia un contenedor
[docker/podman/crictl] cp [PATH_abosluto] [container_ID]:[destino_PATH_absoluto]
- Guardar un contenedor en funcionamiento a una imagen
[docker/podman/crictl] commit [container_ID]
- Renombrar una imagen
[docker/podman/crictl] tag [container_ID] [nombre]:[version_tag]
- Listar volumenes
[docker/podman/crictl] volume ls
Medidas de seguridad y personalización
Los contenedores al igual que los sandbox y las máquinas virtuales tienen la posibilidad de personalizar los tres tipos de aislamientos que vimos anteriormente, haciendo así que el nivel de seguridad suba o baje dependiendo de los cambios que se hagan.
Si sumamos las configuraciones y archivos que deben ser adicionalmente segurizados, tenemos:
-
Dockerfiles
El archivo con nombre “Dockerfile” es uno que contiene dentro los paso a paso que deben realizar docker,podman o cri-o para construir un contenedor de forma automatizada.
En la documentación ( https://docs.docker.com/reference/dockerfile/) podemos ver todas las instrucciones que pueden ser utilizadas en el archivo.
Por ejemplo;
FROM docker.io/archlinux/archlinux:base
RUN pacman -Syyuuq gnupg base-devel micro bash sudo git --noconfirm --needed
RUN rm -r /etc/pacman.d/gnupg && pacman-key --init && pacman-key --populate
ADD makepkg.conf /etc/makepkg.conf
RUN sed -i "s/ParallelDownloads = 2/ParallelDownloads = 20/g" /etc/pacman.conf
RUN useradd -r -u 1000 -d /home/shyanjmc -m -s /usr/bin/bash shyanjmc
RUN echo "shyanjmc ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN su shyanjmc -c "rm -rf ~/.gnupg"
# Firefox
RUN su shyanjmc -c "gpg --recv-key 14F26682D0916CDD81E37B6D61B7B526D98F0353 && rm /home/shyanjmc/.gnupg/public-keys.d/pubring.db.lock"
# LibreOffice
RUN su shyanjmc -c "gpg --recv-key C2839ECAD9408FBE9531C3E9F434A1EFAFEEAEA3 && rm /home/shyanjmc/.gnupg/public-keys.d/pubring.db.lock"
# Repositories
RUN echo "sudo pacman -Syuq --noconfirm" >> /home/shyanjmc/.bashrc
RUN echo "git config --global --add safe.directory /data" >> /home/shyanjmc/.bashrc
VOLUME ["/data"]
CMD ["/usr/bin/bash"]
Como tal, ese archivo es de criticidad para la organización e infraestructura, ya que contiene la “receta” con el que se está construyendo el contenedor. Si el mismo se filtra hacia el exterior estará revelando información interna que pueda ayudar a identificar vulnerabilidades en la compañia.
Nosotros como auditores podemos también ayudar a la seguridad de ese archivo;
-
Debemos verificar siempre que no se estén utilizando tokens (credenciales, etc) en las variables de entorno (ENV) ya que estarán disponible para todos los programas y usuarios en ese contenedor.
-
Debemos verificar que no está deshabilitado el chequeo de salubridad (HEALTHCHECK) mediante la instrucción; HEALTHCHECK NONE .
-
Debemos verificar que se estén utilizando volúmenes (VOLUME) para guardar datos que deben persistir aún si el contenedor muere.
-
Aislamiento de almacenamiento
El contenedor puede estar completamente o parcialmente aislado a nivel de almacenamiento.
Para compartir información en forma de archivos entre el sistema host y el contenedor se debe usar la opción; “-v” ó “–volume” cuando se crea el contenedor.
No se puede editar esta configuración de uno ya en funcionamiento.
Al poderse personalizar se puede hacer que un directorio específico del host sea visible dentro del contenedor en una carpeta específica. A esto se le llama “volumen” y lo que hace internamente Linux es; se refleja (‘mirror’) todo el contenido de una carpeta en el sistema host en una carpeta específica del contenedor, al ocurrir un cambio en cualquiera de ambos puntos el engine refleja ese cambio en el otro (este proceso se llama; binding).
Como tal acá un ejemplo;
Esto permite compartir archivos y así mismo resguardar la información que utiliza el contenedor.
Cosas a no realizar nunca con el aislamiento de almacenamiento;
-
Jamás se debe montar todo el sistema operativo host (véase; “/”) en el contenedor. Esto hace que si el contenedor es vulnerado, se tiene acceso a todos los archivos del host.
-
Jamás se debe deshabilitar el aislamiento de RAM montando los directorios; “/proc” y “/dev” como espejos del host en el contenedor. Esto hace que si el contenedor es vulnerado, podrá consultar que programas hay en funcionamiento y que hardware se está usando.
Con el comando de ‘inspect’ podemos visualizar la configuración ya existente de los volúmenes en un contenedor;
En caso de que no hallamos especificado volúmenes, pero en el dockerfile que creó ese contenedor si estuvieran, los volúmenes en el host estarán en;
- docker; /var/lib/docker/volume
- crio; /var/lib/containers/volume
- podman; /home/[usuario]/.local/share/containers/storage/
-
-
Aislamiento de red
Como comentamos anteriormente, el aislamiento de red en un contenedor hace que el mismo tenga su propio punto de entrada/salida hacia la misma.
Como tal, la interfaz virtual puede modificarse en varios modos;
-
Por defecto no se puede acceder a un puerto en el contenedor desde fuera del mismo. Si se quiere, se debe modificar el aislamiento de red para redirigir un puerto del host hacia el contenedor con los argumentos “-p” ó “–port”;
-
Si se quiere deshabilitar el aislamiento de red se debe usar el argumento “–network host”. El cual abrirá el puerto en el host como si no estuviera en el contenedor.
Desaconsejo enormemente esto, ya que desde dentro del contenedor se podría detectar si un puerto está en uso o no y por que en caso de ser vulnerado, se podrá abrir puertos (Ejem; RAT, Troyanos, etc) accesibles.
Con el comando de ‘inspect’ podemos visualizar la configuración ya existente de la red en un contenedor;
-
-
Uso del Mandatory Access Control
Como hemos visto anteriormente, podemos utilizar dos sistemas MAC principales en los sistemas Linux; AppArmor y SELinux.
Todos los engine compatibles con el estándar OCI pueden utilizar perfiles de segurida para AppArmor;
[docker/podman/crictl] …. --security-opt apparmor=[apparmor_profile_name] …
Así mismo jamás debe usar la opción del argumento “--security-opt label=nested” ya que esto permitirá que se pueda modificar SELinux desde dentro del contenedor, por lo que en caso de ser vulnerado se podria modificar el esquema de seguridad de toda la distribución.
-
Restricción de contenedores en podman
Si usamos podman podemos limitar la posiblidad de que el contenedor ejecutándose con nuestros permisos pueda escalar hacia root (o permisos administrativos específicos).
La limitación permitirá aumentar la seguridad del contenedor, aislándolo mejor;
podman … --security-opt=no-new-privileges …
-
Segurización del socket tipo Unix
Un socket permite una conectividad entre dos programas diferentes.
Los sockets de tipo Unix de los engines se almacenan en;
- crio; /run/crio/crio.sock
- docker; /run/containerd/containerd.sock
- podman; /run/podman/podman.sock
Cualquier programa que tenga acceso a esos sockets podrá interactuar con el engine respectivo.
En caso de docker y crio, si no es root o si el usuario no está en el grupo wheel no le permitirá interactuar.
En caso de podman, la respuesta será dependiendo del usuario que lo solicita.
-
Versionado de la imagen
Puede parecer una cuestión superflua, mínima pero no.
Por defecto cuando uno no especifica la versión (‘tag’) de una imagen, se usa la “latest”.
Esto conlleva que la versión latest es siempre la última disponible, con lo que si se está usando una imagen de Rocky Linux 8 y se descarga la que tiene tag “latest” se estará usando la última actualmente que es la 9.4 y habrá problemas por las diferencias de versionado en paquetes.
Por lo tanto, se debe utilizar siempre tags/versiones específicas en distros ‘Fixed Release’.
Cuando sean contenedores personalizados, o un backup de uno ya existente, se debe seguir la nomenclatura semántica de versionado;
-
Versionado semántico
vX.Y.Z
X: Versión mayor, entre versiones mayores no hay retro-compatibilidad.
Y: Versionado menor, aumenta en uno (1) cada vez que se añade, saca o actualiza algo.
Z: Parche, aumenta en uno (1) cada vez que se corrigen errores en los programas, archivos de configuración, etc.
Esto, en conjunto con una buena documentación, permite la correcta identificación de las versiones de cada contenedor utilizado en la infraestructura.
-
Virtualización
La virtualización es un tipo de aislamiento que utiliza tanto capacidades del procesador como del kernel.
Al igual que los contenedores, se realizan los tres tipos de aislamientos que ya conocemos; almacenamiento, RAM y red. Pero se añade una capa importante; mientras que los contenedores solamente pueden ejecutar un entorno aislado que dependan del mismo kernel del OS host (véase; no puedes crear un contenedor de Windows en un sistema Linux y viceversa. Ya que los kernels son incompatibles.), la virtualización crea una capa adicional entre el sistema host que almacena todo y entre el sistema invitado (que es el virtualizado) que emula/simula hardware de una forma determinada para crear, justamente, una “máquina virtual”.
Soporte del hardware
El soporte de virtualización proviene, principalmente, por parte del hardware;
El CPU tiene tecnologías que permiten que no resida toda la responsabilidad del proceso de virtualización en el núcleo del sistema. Estas tecnologías le permiten al procesador poder identificar una máquina virtual y por ende, identificarse de cara a cada una (la VM; Virtual Machine) como un (o varios) vCPU (virtual CPU).
Internamente; el CPU tiene a nivel de mnemotécnicos (osea; las instrucciones en lenguaje ensamblador) instrucciones específicas para poder manejar cada tabla de memoria de cada máquina virtual como si de un proceso más se tratara. Las tablas de memoria están aisladas siempre, sean máquinas virtuales o no, por lo que se aprovecha el aislamiento de memoria natural del kernel pero a un nivel más bajo.
Arquitectura | Procesador | Nombre de la tecnología de soporte |
---|---|---|
x86_64 | Intel | Intel-VT |
x86_64 | AMD | AMD-V |
ARM | ARM | Virtualization Extensions, GICv2 Virtualization Extensions |
Hypervisor
El hypervisor es el componente de software esencial dentro de todo el proceso de virtualización.
Es el encargado de comunicarse a bajo nivel con el kernel del sistema operativo host para realizar correctamente la comunicación con el CPU y manejar correctamente los aislamientos de cada máquina virtual, a fin de que los aislamientos no se rompan y a la vez se sigue manteniendo la emulación de hardware virtual.
Según el tipo de hypervisor, funcionarán de una forma u otra.
-
Tipo 1: Bare Metal
Los hypervisor de tipo 1 son conocidos como ‘bare metal’. Como su nombre indica, al ser bare metal pueden ejecutarse directamente en el hardware (como si ellos mismos fueran un OS completo, que lo son). Por ende, no dependen de un sistema operativo previo para poder ejecutarse correctamente.
Entre esta categoría se encuentra; Xen.
Nota curiosa Una distribución Linux que utiliza Xen es; Qubes OS. Probablemente una de las distribuciones más seguras. -
Tipo 2: Hosted
Los hypervisor de tipo 2 son conocidos como ‘almacenados’. Como su nombre indica, necesitan ser almacenados y ejecutados por un sistema operativo ( previamente en funcionamiento ) como un programa más para poder funcionar correctamente.
Entre esta categoría se encuentran; KVM (el módulo que convierte al kernel Linux en un hypervisor), VirtualBox, VMWare, Qemu, HyperV, Parallels Desktop, etc.
Virtualización completa y paravirtualización
Quizás parezcan lo mismo pero no, son diferentes.
Estrictamente hablando; la virtualización completa emula hardware virtual para engañar al sistema operativo invitado y hacerle creer que está instalado bare-metal (véase; como si no estuviera virtualizado). Mientras que en la paravirtualización (término popularizado por Xen) se informa al propio sistema operativo invitado que está en un ambiente virtual y se usan drivers específicamente optimizados para ese hardware virtual.
La paravirtualización se aplica principalmente a; GPU, Ethernet, sonido y USB. El que se use la paravirtualización permite crear dispositivos virtuales emulados y drivers para tales mucho más optimizados, a fin de ofrecer un mejor rendimiento. Pero a nivel de seguridad, si un atacante o un troyano (habiendo ganado acceso al sistema operativo invitado) detectan esos drivers / dispositivos, entonces se enterarán que están en una máquina virtual.