Skip to content

Ola 5 · Sebas 5.9: llaves de API con alcance (backend) - #16

Open
heysebitas wants to merge 1 commit into
feat/o2-2.11-idempotenciafrom
feat/o5-5.9-llaves-api
Open

heysebitas wants to merge 1 commit into
feat/o2-2.11-idempotenciafrom
feat/o5-5.9-llaves-api

Conversation

@heysebitas

Copy link
Copy Markdown
Collaborator

Tarea 5.9 · va sobre #15 · backend completo, vista pendiente.

El problema

El HIS de un hospital solo podía hablarle a core con la contraseña de turno, que puede todo: crear casos, aceptar traslados, declarar capacidad. Darle eso a una integración es darle a un sistema ajeno el botón de aceptar pacientes.

Qué trae

  • pulso_sk_ + 32 bytes aleatorios. El prefijo no es cosmético: lo detectan los escáneres de secretos si alguien la commitea. Cuesta cero y evita el peor final.
  • Se guarda solo el sha256 y los últimos 4 caracteres. El valor se muestra una vez; si se pierde, se rota.
  • Alcance por llave, mínimo por defecto: sin alcances explícitos no hace nada. Y una ruta sin @Alcance() no la admite ninguna llave, aunque el actor tenga el rol servicio — el mínimo por defecto vale también para las rutas.
  • Rotación con 24 h de gracia. Revocar en el acto convierte "rotar llaves" en algo que nadie quiere hacer, porque el corte lo causa nuestro botón.
  • Revocación inmediata, y los intentos con una llave revocada se registran: esa es justo la señal de que alguien todavía la tiene.
  • Uso por llave (conteo, última vez, IP). Cada llave entra al límite de tasa de 2.11 con su propio cubo, porque su actor es llave:<id>.
  • GET /estado abierto a caso:leer como primera ruta consumible.

Por qué sha256 aquí y Argon2id en las contraseñas

No es una inconsistencia. Argon2id/scrypt existen para hacer lento el ataque por diccionario contra secretos de baja entropía —los que elige una persona—. Una llave de 32 bytes aleatorios no tiene diccionario que la contenga: hashearla con una KDF lenta solo cobraría 50 ms de CPU por cada petición del HIS. Lo que sí importa aquí es que el valor no se guarda nunca y que la comparación sea en tiempo constante.

Hecho cuando

  • Una llave solo hace lo de su alcance (403 probado end-to-end)
  • La rotación no tumba al integrador
  • El uso queda registrado
  • La llave no se puede volver a ver

12 tests nuevos. Verificado end-to-end: crear, listar sin filtrar el secreto ni el hash, 403 por alcance insuficiente, 403 en ruta sin @Alcance(), rotar sin cortar, revocar, y 404 —no 403— para una llave de otra organización (confirmar que existe le diría a un administrador ajeno que esa llave es de alguien).

⚠️ Antes de repartir una llave fuera del equipo

Dos cosas que no dependen de mí:

  1. GET /estado todavía no filtra por organización — una llave con caso:leer ve todo lo que hay en memoria. Lo cierra el aislamiento de inquilino (1.5 Zaid / 1.6 Neid).
  2. Las llaves viven en memoria hasta que exista su tabla (1.2 Neid). Reiniciar core las invalida todas — el lado seguro del fallo.

Falta la vista /panel/api: cuelga del shell de /panel (2.7), que a su vez espera 1.4 (Juan). Mientras tanto se administra con curl, que es como lo va a usar el integrador de un HIS de todas formas.

🤖 Generated with Claude Code

El HIS de un hospital solo podia hablarle a core con la contraseña de turno,
que puede TODO: crear casos, aceptar traslados, declarar capacidad. Darle eso
a una integracion es darle a un sistema ajeno el boton de aceptar pacientes.

  - `pulso_sk_` + 32 bytes aleatorios. El prefijo no es cosmetico: lo detectan
    los escaneres de secretos si alguien la commitea.
  - Se guarda solo el sha256 y los ultimos 4 caracteres. El valor se muestra
    UNA vez; si se pierde, se rota. (sha256 y no Argon2id a proposito: una
    llave de 32 bytes aleatorios no tiene diccionario que la contenga, y una
    KDF lenta solo cobraria 50 ms de CPU por peticion del HIS.)
  - Alcance por llave, **minimo por defecto**: sin alcances explicitos no hace
    nada. Y una ruta sin `@Alcance()` no la admite ninguna llave, aunque el
    actor tenga el rol `servicio`.
  - Rotacion con 24 h de gracia: revocar en el acto convierte "rotar llaves"
    en algo que nadie quiere hacer, porque el corte lo causa nuestro boton.
  - Revocacion inmediata, y los intentos con una llave revocada se registran:
    esa es justo la señal de que alguien todavia la tiene.
  - Uso por llave (conteo, ultima vez, IP) — lo que permite detectar una
    filtrada. Cada llave entra al limite de tasa de 2.11 con su propio cubo,
    porque su actor es `llave:<id>`.
  - `GET /estado` abierto a `caso:leer` como primera ruta consumible.

⚠️ Falta la vista `/panel/api`: cuelga del shell de /panel (2.7 → 1.4). Se
   administra con curl mientras tanto. Y antes de repartir una llave fuera del
   equipo hay que cerrar el aislamiento por organizacion de /estado (1.5/1.6).

Verificado end-to-end: crear, listar sin filtrar el secreto, 403 por alcance
insuficiente, 403 en ruta sin @alcance, rotar sin cortar, revocar, y 404 —no
403— para una llave de otra organizacion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant