Expectativas erroneas con login.conf

2023/04/23

TL;DR: no modificar el archivo /etc/login.conf

Auditoría de seguridad

A partir de un blog que leí hace unos cuantos días me enteré de la existencia del paquete lynis disponible para openbsd.

De la descripción del paquete:

Security auditing tool for Linux, Mac and Unix based systems. Scan your systems in a matter of minutes and know what can be improved.

Así que pensé ¿porqué no probarlo?

Luego de instalarlo (doas pkg_add lynis) y realizar un escaneo, uno de los detalles que obtengo es:

2023-04-01 01:41:58 Result: file /etc/login.conf exists
2023-04-01 01:41:58 Hardening: assigned partial number of hardening points (0 of 2). Currently having 3 points (out of 8)
2023-04-01 01:41:58 Result: found umask value 022, which can be more strict
2023-04-01 01:41:58 Suggestion: Umask in /etc/login.conf could be more strict like 027 [test:AUTH-9328] [details:-] [solution:-]

Por supuesto, cualquier resultado obtenido por una herramienta de auditoría debe evaluarse y no aceptarse ciegamente, pero en este caso parece razonable la sugerencia.

Revisando el archivo /etc/login.conf para ver el valor de umask tenemos que solo se define en un lugar, la clase default:

$ grep -C3 umask /etc/login.conf
#
default:\
	:path=/usr/bin /bin /usr/sbin /sbin /usr/X11R6/bin /usr/local/bin /usr/local/sbin:\
	:umask=022:\
	:datasize-max=1024M:\
	:datasize-cur=1024M:\
	:maxproc-max=256:\

Para modificarla nada más sencillo que hacer:

$ doas sed -i.orig 's/umask=022/umask=027/' /etc/login.conf

Nota: siempre que se modifique un archivo de configuración tener abierta una terminal con el usuario root y no cerrarla hasta corroborar que el cambio haya sido satisfactorio.

Y con ello ya aumentamos la seguridad del sistema.

Problemas de permisos en /etc/resolv.conf

A los días y sin darme cuenta que el fué debido al aumento del hardening del sistema, empiezo a tener problemas para conectarme a internet del tipo:

$ ping www.openbsd.org
ping: no address associated with name

Luego de buscar un poco en internet, encuentro que el problema se debe a los permisos del archivo /etc/resolv.conf:

$ ls -lh /etc/resolv.conf
-rw-r----- 1 root wheel 98 Apr  9 21:35 /etc/resolv.conf

Lo cual se corrige fácilmente utilizando:

$ doas chmod 644 /etc/resolv.conf

Lo malo es que periódicamente se resetean los permisos del archivo a 0640.

¿Quien modifica los permisos de /etc/resolv.conf?

Lo cierto es que luego de cansarme de estar utilizando chmod y viendo que no es algo puntual, me embarco en buscar quien está modificando los permisos de /etc/resolv.conf.

Luego de un tiempo buscando en internet, me pongo a hacer grep en el código de openbsd, con lo cual encuentro a quien creo el culpable: ./sbin/resolvd/resolvd.c. Inspeccionando el código en el archivo encuentro que resolvd crea un nuevo archivo cada vez que me conecto/desconecto a la wifi con permiso 0640. Esto es razonable, ya que el hardening configurado previamente indica que el umask debe ser 027 en lugar de 022.

¿Mi primer idea de solución?

diff --git sbin/resolvd/resolvd.c sbin/resolvd/resolvd.c
index 2ffdfc6ddb4..133559819f6 100644
--- sbin/resolvd/resolvd.c
+++ sbin/resolvd/resolvd.c
@@ -192,6 +192,8 @@ main(int argc, char *argv[])
 	if (geteuid())
 		errx(1, "need root privileges");

+	umask(0022);
+
 	lockfd = open(_PATH_LOCKFILE, O_CREAT|O_RDWR|O_EXLOCK|O_NONBLOCK, 0600);
 	if (lockfd == -1) {
 		if (errno == EAGAIN)

Esto basado en la modificación de usr.sbin/pkg_add/OpenBSD/AddDelete.pm:

sub do_the_main_work
{
	my ($self, $state) = @_;

	if ($state->{bad}) {
		return;
	}

	umask 0022;

Y ya que estamos, ¿porqué no compartir el cambio?

Haciendo corto el intercambio de emails, el parche no tiene sentido conceptualmente. En lugar de modificar el código de unos 20 demonios tendría más sentido resetear el valor de umask en la clase daemon:

--- /etc/login.conf.orig
+++ /etc/login.conf
@@ -58,6 +58,7 @@
 # Be sure to reset these values to system defaults in the default class!
 #
 daemon:\
+	:umask=022:\
 	:ignorenologin:\
 	:datasize=4096M:\
 	:maxproc=infinity:\

pero ya que al parecer fuí el único (digamos mejor de los pocos) que rompió su sistema por modificar el archivo /etc/login.conf no tiene sentido sellar la clase daemon de cambios en la clase default.

Forma correcta de asignar umask

Ya que aprendí (ver más adelante) que mejor no estar tocando el archivo /etc/login.conf, una buena solución (¿la correcta?) es agregar el archivo /etc/login.conf.d/staff con el contenido:

#
# Staff have fewer restrictions and can login even when nologins are set.
#
staff:\
        :umask=027:\
        :datasize-cur=1536M:\
        :datasize-max=infinity:\
        :maxproc-max=512:\
        :maxproc-cur=256:\
        :ignorenologin:\
        :requirehome@:\
        :tc=default:

el cual es una copia de la definición que se encuentra en /etc/login.conf pero agregando el valor umask=027.

¿Porqué staff? Porque cuando se crea un usuario sería la clase que habría que agregar por defecto. El login class del usuario que es creado por el instalador también se encuentra en dicha clase:

$ doas grep ":$(id -u):" /etc/master.passwd
jmpc:XXX:1000:1000:staff:0:0:JMPC:/home/jmpc:/usr/local/bin/bash

(el formato del archivo se describe en man 5 passwd).

Problemas con comentarios y continuación de linea en getent

Durante la realización de pruebas modificando el archivo /etc/login.conf para corroborar que usando umask=027 en la clase default y umask=022 en la clase daemon resetea el cambio de umask en la clase default y resolvd funciona de la forma esperada, escribí:

--- /etc/login.conf.orig
+++ /etc/login.conf
@@ -57,6 +57,7 @@
 # This must be set properly for daemons started as root by inetd as well.
 # Be sure to reset these values to system defaults in the default class!
 #
+#:umask=022:\
 daemon:\
 	:ignorenologin:\
 	:datasize=4096M:\

Esto es, comenté la linea y la moví arriba de la definición de la clase daemon.

A partir de esto al querer editar el archivo como root utilizando doas obtuve:

$ doas vim /etc/login.conf
doas: failed to set user context for target

Tampoco me fué posible utilizar su -l.

¿Que pasó?

Bueno, la primera hipótesis (que resultó acertada) es que en el archivo /etc/login.conf la continuación de linea (el \ al final) se interpreta en las lineas de comentarios.

Una de las ventajas de openbsd es que el kernel y el código del sistema base se encuentran en el mismo repositorio, por lo cual es bastante sencillo seguir el código que se está ejecutando. También en los archivos .c los nombres de las funciones comienzan en el primer carácter.

Partiendo del mensaje dado por doas:

  1. El mensaje anterior se encuentra en la función main, en el archivo usr.bin/doas/doas.c Este ocurre en caso de un error en setusercontext.

    Aquí ya se puede utilizar el manual para obtener información sobre la función y confirmar que ese es el camino correcto. De man setusercontext:

    login_getclass, login_getstyle, login_getcapbool, login_getcapnum, login_getcapsize, login_getcapstr, login_getcaptime, login_close, setclasscontext, setusercontext — query login.conf database about a user class

  2. La definición de setusercontext se encuentra en lib/libc/gen/login_cap.c Allí vemos que se invoca a login_getclass que se encuentra en el mismo archivo.

  3. A su vez, login_getclass invoca a cgetent.

    Buscando en el manual por cgetent se tiene (man cgetent):

    cgetent, cgetset, cgetmatch, cgetcap, cgetnum, cgetstr, cgetustr, cgetfirst, cgetnext, cgetclose, cgetusedb — capability database access routines

    Y lo más importante:

    Capability database syntax

    Capability databases are normally ASCII and may be edited with standard text editors. Blank lines and lines beginning with a ‘#’ are comments and are ignored. Lines ending with a ‘\’ indicate that the next line is a continuation of the current line; the ‘\’ and following newline are ignored. Long lines are usually continued onto several physical lines by ending each line except the last with a ‘\’.

    Sin duda mi interpretación de lo anterior es que primero se ignoran los comentarios y luego se interpretan las lineas que terminan con \ indicando que la siguiente linea es la continuación de la siguiente, lo que no está ocurriendo.

  4. El código de cgetent se encuentra en lib/libc/gen/getcap.c donde llama a getent.

  5. La función getent se encuentra en el mismo archivo que cgetent y allí es donde finalmente se realiza el análisis del archivo. A primera vista se ve que primero se lee una linea completa (interpretando primero el \ al final de la linea) y recién después si la linea es vacía o comienza con # se ignora.

La definición de getent explica el error que obtuve dejando por error el carácter \ encima de la definición de daemon.

También del código entiendo que no se ignora un comentario si el carácter # no es el primero de la linea. Esto no lo tenía tan presente, ya que otros programas permiten blancos antes del comentario en su archivo de configuración.

¿Conclusiones?