<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Linux Sin Humo (Publicaciones sobre SSSD)</title><link>https://sergiobelkin.com/</link><description></description><atom:link href="https://sergiobelkin.com/categories/sssd.xml" rel="self" type="application/rss+xml"></atom:link><language>es</language><copyright>Contents © 2026 &lt;a href="mailto:sebelk@gmail.com"&gt;sebelk&lt;/a&gt; 
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/"&gt;
&lt;img alt="Creative Commons License BY-NC-SA"
style="border-width:0; margin-bottom:12px;"
src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png"&gt;&lt;/a&gt;
</copyright><lastBuildDate>Sat, 18 Jul 2026 17:41:53 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Particularidades de lastlog</title><link>https://sergiobelkin.com/posts/lastlog-particularidades/</link><dc:creator>sebelk</dc:creator><description>&lt;figure&gt;&lt;img src="https://sergiobelkin.com/images/lastlog-mapa.png"&gt;&lt;/figure&gt; &lt;p&gt;En un RHEL 8 recién instalado, &lt;code&gt;/var/log/lastlog&lt;/code&gt; no llama la atención. En el mismo RHEL 8 unido a un dominio por SSSD, después de que un solo usuario del dominio inicie sesión, &lt;code&gt;ls -l&lt;/code&gt; puede informar un archivo de cientos de gigabytes. &lt;code&gt;du -h&lt;/code&gt; sobre ese mismo archivo devuelve unos pocos kilobytes.&lt;/p&gt;
&lt;p&gt;El archivo es &lt;a href="https://sergiobelkin.com/posts/sparse-files-linux/"&gt;sparse&lt;/a&gt;, y eso explica la diferencia entre los dos números. Lo que explica &lt;em&gt;por qué&lt;/em&gt; es sparse es una sola línea de código.&lt;/p&gt;
&lt;h3 id="el-uid-es-la-direccion"&gt;El UID es la dirección&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/var/log/lastlog&lt;/code&gt; guarda el último inicio de sesión de cada usuario. No tiene índice, ni cabecera, ni claves, ni base de datos. La posición del registro dentro del archivo &lt;strong&gt;es&lt;/strong&gt; el UID. En &lt;code&gt;shadow-utils&lt;/code&gt; el cálculo aparece idéntico en la ruta de lectura y en la de escritura:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;off_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pw_uid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;El archivo se trata como un arreglo en disco cuyo índice es el UID, exactamente igual que &lt;code&gt;array[i]&lt;/code&gt; vive en &lt;code&gt;base + i * sizeof(elemento)&lt;/code&gt;. Para leer el último login de cualquier usuario se hace un &lt;code&gt;fseeko&lt;/code&gt; a ese offset y se lee un registro. Acceso directo, sin parsear nada, sin mantener una estructura auxiliar.&lt;/p&gt;
&lt;p&gt;El &lt;code&gt;(off_t)&lt;/code&gt; merece atención. La conversión está &lt;strong&gt;antes&lt;/strong&gt; de la multiplicación, y no es un detalle cosmético: fuerza a que la operación se haga en 64 bits. Si se multiplicara primero en 32 bits, un UID grande desbordaría antes de convertir. Un UID de 20 000 000 por 292 bytes da unos 5840 millones, que no entra en 32 bits. El offset resultante apuntaría a cualquier parte.&lt;/p&gt;
&lt;h3 id="el-registro-y-por-que-mide-lo-que-mide"&gt;El registro, y por qué mide lo que mide&lt;/h3&gt;
&lt;p&gt;La &lt;code&gt;struct lastlog&lt;/code&gt; no la define &lt;code&gt;shadow&lt;/code&gt;: viene de glibc —se declara en &lt;code&gt;&amp;lt;bits/utmp.h&amp;gt;&lt;/code&gt;, que un programa obtiene incluyendo &lt;code&gt;&amp;lt;lastlog.h&amp;gt;&lt;/code&gt;—. Son tres campos:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Campo&lt;/th&gt;
&lt;th&gt;Tamaño&lt;/th&gt;
&lt;th&gt;Contenido&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ll_time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;td&gt;segundos desde epoch del último login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ll_line&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32 bytes&lt;/td&gt;
&lt;td&gt;terminal: &lt;code&gt;pts/0&lt;/code&gt;, &lt;code&gt;tty1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ll_host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;256 bytes&lt;/td&gt;
&lt;td&gt;host remoto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;292 bytes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ese 292 no es universal, y ahí hay una particularidad que se pasa por alto. El campo &lt;code&gt;ll_time&lt;/code&gt; está definido bajo una condición:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#if __WORDSIZE_TIME64_COMPAT32&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;__uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;__time_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#endif&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;En x86-64 esa macro vale 1, &lt;code&gt;ll_time&lt;/code&gt; ocupa 4 bytes y el registro mide 292. En aarch64 vale 0, &lt;code&gt;ll_time&lt;/code&gt; pasa a ser un &lt;code&gt;__time_t&lt;/code&gt; de 8 bytes, y el registro mide &lt;strong&gt;296&lt;/strong&gt;. Mismo archivo, mismo programa, distinta grilla.&lt;/p&gt;
&lt;p&gt;La consecuencia práctica es que &lt;code&gt;/var/log/lastlog&lt;/code&gt; no es portable entre arquitecturas. Copiado de un x86-64 a un aarch64, cada registro se lee corrido, y no hay número de versión ni campo de tipo que permita detectarlo. Es un volcado binario de una estructura de C, con la endianness y el padding de la máquina que lo escribió. Ese &lt;code&gt;ll_time&lt;/code&gt; condicional además arrastra el problema del año 2038.&lt;/p&gt;
&lt;h3 id="el-nombre-del-usuario-no-esta-guardado"&gt;El nombre del usuario no está guardado&lt;/h3&gt;
&lt;p&gt;Mirá de nuevo la tabla: hay tiempo, terminal y host. No hay nombre. La identidad del usuario no está &lt;em&gt;en&lt;/em&gt; el registro, está en la &lt;strong&gt;posición&lt;/strong&gt; del registro. Si el offset es &lt;code&gt;UID × 292&lt;/code&gt;, entonces &lt;code&gt;UID = offset / 292&lt;/code&gt;, y eso es todo lo que el archivo sabe sobre quién es quién.&lt;/p&gt;
&lt;p&gt;De ahí sale un comportamiento que sorprende cuando aparece. Si dos entradas de &lt;code&gt;/etc/passwd&lt;/code&gt; comparten UID:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;::/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="sr"&gt;/alice:/bin/&lt;/span&gt;&lt;span class="n"&gt;bash&lt;/span&gt;
&lt;span class="n"&gt;bob&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;::/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="sr"&gt;/bob:/bin/&lt;/span&gt;&lt;span class="n"&gt;bash&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;las dos cuentas comparten físicamente el mismo registro. &lt;code&gt;lastlog&lt;/code&gt; recorre &lt;code&gt;/etc/passwd&lt;/code&gt; con &lt;code&gt;getpwent()&lt;/code&gt; y procesa cada entrada por separado, así que imprime &lt;strong&gt;dos filas&lt;/strong&gt;, con nombres distintos y exactamente la misma fecha, la misma terminal y el mismo host. Si entró &lt;code&gt;alice&lt;/code&gt;, la salida muestra a &lt;code&gt;bob&lt;/code&gt; con esa fecha.&lt;/p&gt;
&lt;p&gt;En escritura es peor de razonar. &lt;code&gt;lastlog -S -u bob&lt;/code&gt; hace &lt;code&gt;getpwnam("bob")&lt;/code&gt; para obtener el UID, y desde ahí todo el trabajo se hace por UID. Lo que se actualiza es el registro del UID 1000, indistinguible del de &lt;code&gt;alice&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;No es un defecto de &lt;code&gt;lastlog&lt;/code&gt;. Es lo que significa tratar el UID como identidad, que es exactamente lo que hace el kernel: los permisos, la propiedad de los archivos y las credenciales de proceso van por UID. Los nombres son etiquetas en &lt;code&gt;/etc/passwd&lt;/code&gt;. Dos nombres con el mismo UID son el mismo usuario a todos los efectos, y &lt;code&gt;lastlog&lt;/code&gt; hereda esa ambigüedad en lugar de inventarse una propia.&lt;/p&gt;
&lt;p&gt;Un corolario del mismo diseño: como &lt;code&gt;lastlog&lt;/code&gt; recorre &lt;code&gt;passwd&lt;/code&gt; en vez de recorrer el archivo, los registros de usuarios ya borrados siguen físicamente en disco pero no se listan. Y si &lt;code&gt;nsswitch&lt;/code&gt; está respaldado por SSSD contra un directorio de decenas de miles de usuarios, ese &lt;code&gt;getpwent()&lt;/code&gt; enumera toda la base remota. El cuello de botella pasa a ser NSS, no el archivo.&lt;/p&gt;
&lt;h3 id="leer-es-seguro-escribir-es-lo-que-infla"&gt;Leer es seguro; escribir es lo que infla&lt;/h3&gt;
&lt;p&gt;Hay una asimetría en el código que determina cuándo el archivo crece.&lt;/p&gt;
&lt;p&gt;En &lt;strong&gt;lectura&lt;/strong&gt;, antes de posicionarse se comprueba que el registro entre dentro del archivo:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;statbuf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Si el UID cae más allá del final, no se lee nada: la estructura se llena de ceros con &lt;code&gt;memzero&lt;/code&gt; y se imprime &lt;code&gt;**Never logged in**&lt;/code&gt;. Consultar el &lt;code&gt;lastlog&lt;/code&gt; de un usuario con UID enorme &lt;strong&gt;no&lt;/strong&gt; agranda el archivo. Y como un agujero también se lee como ceros, el código trata igual al "usuario dentro de un agujero" y al "usuario fuera del archivo". Ambos son lo mismo: nadie inició sesión.&lt;/p&gt;
&lt;p&gt;En &lt;strong&gt;escritura&lt;/strong&gt; no hay ninguna comprobación de límites: se hace &lt;code&gt;fseeko&lt;/code&gt; al offset y se escribe. Un solo login —o un solo &lt;code&gt;lastlog -S -u &amp;lt;usuario&amp;gt;&lt;/code&gt;— con un UID alto extiende el archivo hasta ese offset y crea el agujero intermedio.&lt;/p&gt;
&lt;p&gt;Por eso el tamaño aparente del archivo no depende de cuántos usuarios tenés, sino del &lt;strong&gt;UID más alto que alguna vez haya escrito un registro&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="los-numeros-con-un-directorio-corporativo"&gt;Los números con un directorio corporativo&lt;/h3&gt;
&lt;p&gt;Con cuentas locales, los UID rondan el millar y el archivo es irrelevante. Con Active Directory o LDAP vía SSSD y mapeo algorítmico de SID a POSIX ID, el panorama cambia. Los defaults de SSSD ubican el rango de UID mapeados entre &lt;code&gt;ldap_idmap_range_min&lt;/code&gt; = &lt;code&gt;200000&lt;/code&gt; y &lt;code&gt;ldap_idmap_range_max&lt;/code&gt; = &lt;code&gt;2000200000&lt;/code&gt;, repartido en slices de &lt;code&gt;200000&lt;/code&gt;, y el slice que le toca a cada dominio se elige por hash de su SID. Es decir: el UID de un usuario de dominio puede caer en cualquier punto de ese rango.&lt;/p&gt;
&lt;p&gt;A 292 bytes por registro:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;UID que inició sesión&lt;/th&gt;
&lt;th&gt;Tamaño aparente&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 000&lt;/td&gt;
&lt;td&gt;285 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 000 000&lt;/td&gt;
&lt;td&gt;278 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 000 000&lt;/td&gt;
&lt;td&gt;27,2 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;145 000 000&lt;/td&gt;
&lt;td&gt;39,4 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 000 199 999 (tope del rango)&lt;/td&gt;
&lt;td&gt;543,9 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;a class="image-reference" href="https://sergiobelkin.com/images/lastlog-mapa.svg"&gt;&lt;img src="https://sergiobelkin.com/images/lastlog-mapa.svg" alt="Mapa de /var/log/lastlog como arreglo indexado por UID. root y los usuarios de sistema comparten el bloque 0; los UID 999, 1000 y 1001 caen en el mismo bloque 71; nobody en el bloque 4671; un usuario de dominio con UID 145.000.000 aterriza en el bloque 10.336.914. El tamaño aparente resultante es de 39,4 GiB contra apenas 16 KiB reales en disco." style="max-width: 100%; height: auto;"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;El archivo visto como un arreglo en disco: cada login escribe su registro en &lt;code&gt;offset = UID × 292&lt;/code&gt;, y los agujeros entre los UID reales no ocupan disco. La frontera administrativa entre cuentas de sistema y de persona (UID 999/1000) cae dentro de un mismo bloque; un solo login de dominio salta a diez millones de bloques de distancia y fija el tamaño aparente.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;El espacio real en disco sigue siendo proporcional a la cantidad de registros escritos. El disco no se llena. Lo que se rompe es todo lo que lea el archivo secuencialmente.&lt;/p&gt;
&lt;p&gt;El manual lo dice sin vueltas:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The lastlog file is a database which contains info on the last login of each user. &lt;strong&gt;You should not rotate it.&lt;/strong&gt; It is a sparse file, so its size on the disk is usually much smaller than the one shown by "ls -l" (which can indicate a really big file if you have in passwd users with a high UID). You can display its real size with "ls -s".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Rotar el archivo con &lt;code&gt;logrotate&lt;/code&gt; usando &lt;code&gt;copy&lt;/code&gt; o &lt;code&gt;copytruncate&lt;/code&gt; implica leerlo y reescribirlo. Los 543,9 GiB aparentes se convierten en 543,9 GiB reales.&lt;/p&gt;
&lt;p&gt;La misma advertencia aplica al backup a nivel de archivo. La receta es excluir &lt;code&gt;/var/log/lastlog&lt;/code&gt; del job, y si hace falta conservar el dato de auditoría, volcarlo a un archivo normal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;backups&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ese archivo pesa lo que corresponde al padrón real de usuarios. El mismo criterio aplica a &lt;code&gt;/var/log/faillog&lt;/code&gt;, que no comparte el formato del registro pero sí el esquema: su código calcula &lt;code&gt;offset = (off_t) uid * sizeof (fl);&lt;/code&gt; y produce un archivo disperso por la misma razón.&lt;/p&gt;
&lt;h3 id="por-que-wtmp-no-tiene-este-problema"&gt;Por qué &lt;code&gt;wtmp&lt;/code&gt; no tiene este problema&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lastlog&lt;/code&gt; y &lt;code&gt;wtmp&lt;/code&gt; suelen mencionarse juntos, y son modelos opuestos.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lastlog&lt;/code&gt; es una &lt;strong&gt;tabla de estado&lt;/strong&gt;: un slot por usuario, indexado por UID, que se sobrescribe en cada login. Responde "¿cuándo entró por última vez cada usuario?" con acceso directo, y no guarda historia.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wtmp&lt;/code&gt; es un &lt;strong&gt;libro de contabilidad&lt;/strong&gt;: registros &lt;code&gt;struct utmp&lt;/code&gt; que se agregan al final, en orden cronológico. Cada login, logout, arranque o cambio de runlevel suma un registro. Responde "¿qué pasó en este sistema y en qué orden?".&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;lastlog&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;wtmp&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modelo&lt;/td&gt;
&lt;td&gt;arreglo indexado por UID&lt;/td&gt;
&lt;td&gt;log secuencial, append-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escritura&lt;/td&gt;
&lt;td&gt;sobrescribe el slot del UID&lt;/td&gt;
&lt;td&gt;agrega un registro al final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Guarda el nombre?&lt;/td&gt;
&lt;td&gt;no, es la posición&lt;/td&gt;
&lt;td&gt;sí, en &lt;code&gt;ut_user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historial&lt;/td&gt;
&lt;td&gt;solo el último login&lt;/td&gt;
&lt;td&gt;todos los eventos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tamaño&lt;/td&gt;
&lt;td&gt;∝ UID máximo (con agujeros)&lt;/td&gt;
&lt;td&gt;∝ cantidad de eventos (denso)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Sparse?&lt;/td&gt;
&lt;td&gt;sí&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Rotar?&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;sí, con &lt;code&gt;logrotate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;struct utmp&lt;/code&gt; mide 384 bytes y sí incluye el nombre (&lt;code&gt;ut_user&lt;/code&gt;) y un campo &lt;code&gt;ut_type&lt;/code&gt; que clasifica el evento. Por eso &lt;code&gt;last&lt;/code&gt; puede reconstruir sesiones completas y mostrar reinicios, algo que &lt;code&gt;lastlog&lt;/code&gt; no puede hacer ni en principio. Crece linealmente con los eventos, así que hay que rotarlo; no tiene agujeros, así que se puede.&lt;/p&gt;
&lt;p&gt;Visto desde arriba, &lt;code&gt;lastlog&lt;/code&gt; es un índice materializado del "último &lt;code&gt;USER_PROCESS&lt;/code&gt; por usuario" que podría derivarse recorriendo &lt;code&gt;wtmp&lt;/code&gt;. La ganancia de ese índice es el acceso O(1). El precio es el archivo disperso.&lt;/p&gt;
&lt;h3 id="donde-esta-esto-hoy"&gt;Dónde está esto hoy&lt;/h3&gt;
&lt;p&gt;En la familia Enterprise Linux el esquema sigue siendo el clásico. RHEL 8, 9 y 10 —y sus derivados Rocky, Alma y Oracle Linux— empaquetan el binario &lt;code&gt;lastlog&lt;/code&gt; en &lt;code&gt;shadow-utils&lt;/code&gt; (4.6, 4.9 y 4.15 respectivamente) y mantienen &lt;code&gt;pam_lastlog&lt;/code&gt; en la pila de PAM. El archivo indexado por UID es lo que hay, y lo que hay que recordar excluir del backup y de la rotación.&lt;/p&gt;
&lt;p&gt;RHEL 10 tiene una particularidad que conviene no dar por sentada. Trae &lt;code&gt;util-linux&lt;/code&gt; 2.40, que es la versión donde upstream incorporó &lt;code&gt;liblastlog2&lt;/code&gt;, pero el paquete se compila con &lt;code&gt;--disable-liblastlog2&lt;/code&gt;: no hay binario &lt;code&gt;lastlog2&lt;/code&gt; ni módulo &lt;code&gt;pam_lastlog2&lt;/code&gt; en los repositorios. Tener la versión no implica tener la funcionalidad.&lt;/p&gt;
&lt;p&gt;Lo que sí cambió en RHEL 10 es el anuncio. Red Hat declaró deprecadas las interfaces &lt;code&gt;utmp&lt;/code&gt; y &lt;code&gt;utmpx&lt;/code&gt; de glibc y avisó que se eliminan en RHEL 11. El motivo que da es el desbordamiento de su contador en 2106; el de &lt;code&gt;lastlog&lt;/code&gt;, con su &lt;code&gt;ll_time&lt;/code&gt; de 32 bits con signo, vence bastante antes.&lt;/p&gt;
&lt;p&gt;Fedora ya hizo el cambio. Desde Fedora 43 el default del sistema es &lt;code&gt;lastlog2&lt;/code&gt;, con &lt;code&gt;pam_lastlog2&lt;/code&gt; en la pila de PAM. En una Fedora 44 al día, el paquete &lt;code&gt;shadow-utils&lt;/code&gt; ya no provee un binario &lt;code&gt;lastlog&lt;/code&gt;; el que hay es &lt;code&gt;lastlog2&lt;/code&gt;, y viene de &lt;code&gt;util-linux&lt;/code&gt;. Los datos están en &lt;code&gt;/var/lib/lastlog/lastlog2.db&lt;/code&gt;, una base SQLite indexada por nombre de usuario, y &lt;code&gt;/var/log/lastlog&lt;/code&gt; queda como un archivo vacío de 0 bytes.&lt;/p&gt;
&lt;p&gt;El esquema de esa base dice, en una línea, todo lo que cambió:&lt;/p&gt;
&lt;p&gt;&lt;a class="image-reference" href="https://sergiobelkin.com/images/lastlog2-schema.png"&gt;&lt;img src="https://sergiobelkin.com/images/lastlog2-schema.png" alt="Sesión de sqlite3 en Fedora 44 sobre la base lastlog2.db. El comando .schema devuelve: CREATE TABLE Lastlog2(Name TEXT PRIMARY KEY, Time INTEGER, TTY TEXT, RemoteHost TEXT, Service TEXT);" style="max-width: 100%; height: auto;"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Name TEXT PRIMARY KEY&lt;/code&gt;: el nombre del usuario está guardado y es la clave. La identidad dejó de ser la posición, que es justamente lo que hacía sparse al archivo viejo y lo que provocaba las colisiones de UID. Sin índice posicional no hay agujeros que llenar ni tamaño aparente que explicar. &lt;code&gt;Time INTEGER&lt;/code&gt; es un entero de 64 bits, sin la condicional por arquitectura ni el tope de 2038. Y aparece un campo que la &lt;code&gt;struct&lt;/code&gt; no tenía: &lt;code&gt;Service&lt;/code&gt;, el servicio PAM que originó la sesión.&lt;/p&gt;
&lt;p&gt;En cualquiera de los dos casos, qué esquema tenés a mano se comprueba con un comando:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rpm -ql shadow-utils | grep -i lastlog
&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="fuentes-y-mas-recursos"&gt;Fuentes y más recursos&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/lastlog.8.html"&gt;&lt;code&gt;lastlog(8)&lt;/code&gt;&lt;/a&gt; — la fuente de la advertencia "You should not rotate it" y de la nota sobre el archivo sparse y &lt;code&gt;ls -s&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/utmp.5.html"&gt;&lt;code&gt;utmp(5)&lt;/code&gt;&lt;/a&gt; — define &lt;code&gt;struct utmp&lt;/code&gt;, sus campos &lt;code&gt;ut_type&lt;/code&gt; y &lt;code&gt;ut_user&lt;/code&gt;, y la diferencia entre &lt;code&gt;/run/utmp&lt;/code&gt; (sesiones activas) y &lt;code&gt;/var/log/wtmp&lt;/code&gt; (historial).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 5 sssd-ldap&lt;/code&gt; — sección &lt;em&gt;ID MAPPING&lt;/em&gt;: los defaults &lt;code&gt;ldap_idmap_range_min&lt;/code&gt;, &lt;code&gt;ldap_idmap_range_max&lt;/code&gt; y &lt;code&gt;ldap_idmap_range_size&lt;/code&gt; que ubican los UID de dominio en el rango alto.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fedoraproject.org/wiki/Changes/Migrate_to_lastlog2"&gt;Fedora Change: Migrate to lastlog2&lt;/a&gt; — la migración a &lt;code&gt;lastlog2&lt;/code&gt; como default del sistema, desde Fedora 43.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/10.0_release_notes/deprecated-features"&gt;RHEL 10 — Deprecated functionality&lt;/a&gt; — la deprecación de las interfaces &lt;code&gt;utmp&lt;/code&gt; y &lt;code&gt;utmpx&lt;/code&gt; de glibc, con la eliminación anunciada para RHEL 11.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/redhat/centos-stream/rpms/util-linux/-/raw/c10s/util-linux.spec"&gt;&lt;code&gt;util-linux.spec&lt;/code&gt; de CentOS Stream 10&lt;/a&gt; — el &lt;code&gt;--disable-liblastlog2&lt;/code&gt; que deja a RHEL 10 sin &lt;code&gt;lastlog2&lt;/code&gt; pese a tener util-linux 2.40.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/pam_lastlog2.8.html"&gt;&lt;code&gt;pam_lastlog2(8)&lt;/code&gt;&lt;/a&gt; — el módulo PAM que reemplaza a &lt;code&gt;pam_lastlog&lt;/code&gt;: Y2038-safe y con backend SQLite3, en util-linux desde 2.40.&lt;/li&gt;
&lt;/ul&gt;</description><category>filesystems</category><category>lastlog</category><category>sparse-files</category><category>SSSD</category><category>sysadmin</category><guid>https://sergiobelkin.com/posts/lastlog-particularidades/</guid><pubDate>Fri, 17 Jul 2026 10:00:00 GMT</pubDate></item></channel></rss>