Cómo está hecho KinAuth

KinAuth guarda las identidades de una organización y decide quién entra en sus aplicaciones. Esta página es para quien quiera ver lo que hay detrás de «seguro por diseño»: de qué está hecho el servidor y cómo se sostiene cada garantía.

De qué está hecho

  • El servidor está escrito en Go, sobre su biblioteca estándar y un solo módulo más: golang.org/x/crypto, del propio equipo de Go, con el único paquete de golang.org/x/sys que necesita. Su código fuente está guardado en nuestro repositorio.
  • El panel tiene una sola dependencia: Preact, dos archivos copiados en el repositorio y fijados por su hash.
  • No hay npm ni empaquetador, y compilarlo no descarga nada de la red.
  • Lo demás que necesita lo hemos escrito nosotros: el controlador de PostgreSQL, SAML con sus firmas XML, OpenID Connect con la firma de tokens que lleva debajo, los códigos de la app de autenticación, las llaves de acceso (passkeys) con el CBOR en el que vienen escritas, y el codificador de códigos QR.
  • Lo único que no escribimos nunca es una primitiva criptográfica. Las funciones hash, los cifrados, las firmas, la derivación de claves y TLS son los de la biblioteca estándar o los de x/crypto.

Su propio controlador de PostgreSQL

El controlador es nuestro, y hace pocas cosas, a propósito.

  • TLS siempre: TLS 1.3, con el certificado y el nombre del servidor verificados. No hay opción para desactivarlo.
  • Solo SCRAM-SHA-256, verificando la prueba del servidor. A una base de datos que pide la contraseña en claro o en MD5 se la rechaza antes de enviarle nada, y también a la que deja entrar al controlador sin autenticación: no ha demostrado que sea la nuestra.
  • Una sentencia por consulta, con sus valores como parámetros, por la propia forma de los mensajes que envía el controlador. La única excepción son las migraciones del esquema, que van compiladas en el binario.

Las organizaciones, separadas dos veces

Dos capas, y cada una basta por sí sola.

  • En PostgreSQL, toda tabla con datos de una organización tiene seguridad a nivel de fila, activada y forzada, con una sola política: la fila es de la organización para la que se abrió la transacción en curso. Si no hay ninguna fijada, la política no coincide con nada, así que una consulta que olvida su organización no recibe todas las filas: no recibe ninguna.
  • Las claves foráneas incluyen la organización, de modo que una fila no puede referirse a la de otra organización, sean cuales sean los identificadores que dé la aplicación.
  • En la aplicación, una consulta sobre los datos de una organización solo se puede escribir dentro de una transacción abierta para esa organización, que sale del nombre de host de la petición y nunca de los datos de la petición. Además, cada consulta filtra por la organización.
  • El servidor funciona con un rol de base de datos que no es propietario de nada. Al arrancar comprueba que su rol no es superusuario, que no puede saltarse la seguridad a nivel de fila, que no puede crear roles ni bases de datos, que no es propietario de ninguna tabla y que toda tabla a la que llega tiene seguridad a nivel de fila. Si no es así, no arranca.
  • Cada organización es un nombre de host propio y, por tanto, un origen distinto para el navegador. La cookie de sesión está atada a ese host: presentada en la dirección de otra organización, la sesión no existe.

Contraseñas y sesiones

  • De las contraseñas se guarda su hash con Argon2id, con los parámetros de la RFC 9106 (64 MiB, 3 pasadas, paralelismo 4) y una sal aleatoria de 128 bits. La longitud mínima es 12, sin reglas de composición.
  • Un inicio de sesión hace el mismo trabajo y da la misma respuesta para una dirección desconocida, una contraseña equivocada y una cuenta suspendida.
  • Los inicios de sesión fallidos se frenan. Para la dirección de correo que se escribe, el quinto fallo la bloquea 30 segundos, y el bloqueo se va doblando hasta 15 minutos; para una dirección de cliente, entre todas las cuentas, el vigésimo la bloquea un minuto, y se va doblando.
  • Las sesiones se guardan en el servidor. El navegador tiene un valor aleatorio de 256 bits en una cookie HttpOnly, Secure, SameSite=Strict y con el prefijo __Host-; la base de datos guarda únicamente su SHA-256. Una sesión termina a las 12 horas, o tras 1 hora sin uso.

Claves y secretos

  • Las claves que firman los tokens y las respuestas SAML se guardan selladas con una clave maestra que no está en la base de datos, con AES-256-GCM de la biblioteca estándar. Una copia de la base de datos, por sí sola, no firma nada.
  • El servidor no arranca sin su clave maestra.
  • Ningún secreto se guarda en claro, salvo los cuatro últimos caracteres de un secreto de cliente, que sirven para distinguir uno de otro. Del que solo hay que comprobar se guarda el hash; el que hay que volver a usar, como la clave de una app de autenticación, se guarda sellado.
  • Un secreto que el servidor crea para que alguien se lo lleve, como los códigos de recuperación o el secreto de cliente de una aplicación, se muestra una sola vez, cuando se crea.

Inicio de sesión único

  • OpenID Connect: solo el flujo de código de autorización. No hay flujo implícito ni concesión por contraseña. PKCE con S256 se exige siempre a un cliente público, y a uno confidencial salvo que un administrador lo desactive para esa aplicación.
  • Los tokens de identidad se firman con ES256 o RS256, y con nada más. Un token de renovación se gasta al usarlo, y uno ya gastado que se presenta de nuevo revoca la familia entera. Cada organización es su propio emisor.
  • SAML 2.0: un proveedor de identidad por aplicación, cada uno con su propia clave de firma, de modo que la respuesta para una aplicación no es la de otra. La respuesta va por HTTP-POST a una dirección registrada para esa aplicación; no se envía nada a una dirección tomada de una petición.
  • El acceso se vuelve a preguntar cada vez que se emite un token: un usuario activo, una aplicación activa, acceso por grupo.

Segundos factores

  • Aprobación en el teléfono. El dispositivo crea una clave P-256 y el servidor guarda su mitad pública: nada de lo que hay en la base de datos puede aprobar un inicio de sesión.
  • Cada desafío lleva un valor de un solo uso y un número de dos dígitos. La página de inicio de sesión muestra el número; al dispositivo se le ofrecen tres y nunca se le dice cuál es el bueno, y aprobar con el equivocado es denegar. Así, un inicio de sesión que ha provocado otra persona no se puede aprobar por inercia. Un desafío caduca a los 90 segundos y se responde una sola vez.
  • Toda petición que hace un dispositivo ya añadido va firmada: el método, el host, la ruta, la hora y el hash del cuerpo, cada campo precedido de su longitud para que dos peticiones no compartan mensaje. El host va firmado, así que una petición vale para una sola organización, y su hora no puede desviarse más de 60 segundos. Al añadir el dispositivo, lo que se firma es el host, su código de un solo uso y la clave nueva.
  • Llaves de acceso. La parte de confianza (relying party) es el host de la propia organización, nunca un dominio por encima. Una llave de acceso es el segundo paso después de la contraseña, o inicia sesión ella sola cuando verifica a su usuario.
  • Los códigos de la app de autenticación siguen la RFC 6238. Los códigos de recuperación son diez, de 80 bits aleatorios, de un solo uso cada uno y guardados como hash.

Un registro de auditoría en el que solo se añade

  • Cada inicio de sesión, y cada cambio en los usuarios, los grupos, los dispositivos, los ajustes de la organización, las aplicaciones y sus claves, queda anotado con quién, cuándo, desde qué dirección y qué.
  • La entrada se escribe en la misma transacción que el cambio: o pasan los dos o no pasa ninguno.
  • En el registro solo se añade: la base de datos rechaza una modificación o un borrado a todos los roles que usan el servidor y su esquema.
  • Ninguna entrada guarda un secreto, un código ni un desafío; de un secreto de cliente, solo sus cuatro últimos caracteres.

El panel y el navegador

  • El panel son archivos estáticos compilados dentro del binario del servidor. Nunca ve la sesión, y muestra cada valor como texto: en ningún sitio se construye HTML a partir de datos.
  • Una política de seguridad de contenido (CSP) en cada respuesta admite solo los scripts y los estilos de este mismo origen: sin código en línea, sin eval, sin poder mostrarse dentro de un marco. Se exigen Trusted Types sin permitir ninguna política, así que asignar una cadena a innerHTML es un error.
  • Una petición a la API que llega de otro origen y cambiaría algo se rechaza. Los puntos de entrada de los protocolos, a los que otros sitios tienen que llegar, no leen ninguna cookie.
  • Del panel no se fía nadie. Oculta lo que un usuario no puede hacer; quien lo rechaza es la API.

Pruebas que intentan romperlo

  • Las garantías las sostienen pruebas que intentan romperlas: leer y escribir las filas de otra organización, saltarse la seguridad a nivel de fila con el propio rol del servidor, modificar el registro de auditoría, una base de datos que se salta la autenticación, peticiones desde otro origen, marcado HTML en los nombres.
  • Las pruebas se ejecutan contra un PostgreSQL de verdad, sobre TLS. Una prueba que no llega a su base de datos falla; no se salta.
  • Los protocolos que hemos escrito se prueban con los vectores de sus especificaciones cuando estas los dan (SCRAM, la firma de tokens, PKCE, los códigos de la app de autenticación), y contra otra implementación cuando no: xmlsec1 para las firmas de SAML y el autenticador de Chrome para las llaves de acceso.