Saltar a contenido

API HTTP

Todas las rutas HTTP que sirve kubelatch, quién puede llamarlas y qué hacen. La interfaz web usa esta misma API; puedes llamarla con curl con una cookie de sesión.

Quién puede llamar

Acceso Qué hace falta
Anónimo Nada.
Cookie de unión La cookie que deja el callback de OIDC durante diez minutos cuando entra alguien sin cuenta: __Host-kubelatch_join con una URL base https, kubelatch_join sobre http sin cifrar. Solo en /api/join (Incorporación).
Sesión La cookie de sesión de una persona con la cuenta habilitada: __Host-kubelatch_session con una URL base https, kubelatch_session sobre http sin cifrar. Sin ella, 401.
Admin Sesión de una persona con rol de administrador. Sin el rol, 403.
Bearer klt_ Authorization: Bearer klt_…, una credencial de kubelatch.
Token de la CLI Authorization: Bearer klt_… con la credencial de un kubelatch login (tipo cli). Solo en las rutas marcadas «Token de la CLI»; cualquier otra ruta de /api responde 403 cli.scope. Nunca cuenta como admin.
Token de agente Authorization: Bearer klt_… con el token de acceso que recibe un agente de IA delegado al iniciar sesión (tipo agent). Solo vale en /mcp: el proxy y /api responden 401.
Token OIDC de Actions El id_token de un job de GitHub Actions en el cuerpo.
Firma de GitHub Cabecera X-Hub-Signature-256 con el HMAC del cuerpo y GITHUB_WEBHOOK_SECRET.

Convenciones de /api

  • Cuerpos JSON de hasta 64 KiB. Se rechazan los campos desconocidos y los datos sobrantes (400); un cuerpo mayor es 413.
  • Todas las respuestas llevan Cache-Control: no-store.
  • Los errores son {"error": "<texto en inglés>", "code": "<código>", "params": {…}} (ver Errores).
  • Una ruta desconocida bajo /api/ es 404; una ruta conocida con otro método es 405 con cabecera Allow.
  • Las peticiones que modifican (POST, PUT, PATCH, DELETE) enviadas por un navegador desde otro origen se rechazan con 403. curl y otros clientes que no son navegadores pasan.
  • Toda respuesta fuera del proxy lleva una cabecera X-Request-Id con un UUIDv7 nuevo. El valor que envíe el cliente se ignora. El mismo id va en la línea del log de acceso del servidor: ver El log del servidor.

Salud

Método Ruta Acceso Qué hace
GET /healthz Anónimo 200 ok si el proceso está vivo.
GET /readyz Anónimo 200 ok si Postgres responde en 2 s; si no, 503 database unavailable.

Solo admiten GET y HEAD; cualquier otro método es 405.

Proxy

Método Ruta Acceso Qué hace
Cualquiera /clusters/<id>/<ruta de la API de Kubernetes> Bearer klt_ Reenvía la petición al API server del cluster <id> actuando como el dueño de la credencial (suplantación). /clusters/<id> y /clusters/<id>/ equivalen a /. Ignora las cookies. Ver El viaje de una petición.

Los errores del proxy son objetos Status de Kubernetes, para que kubectl los muestre bien (Errores del proxy). /clusters sin barra responde 404.

CI y GitHub (fuera de /api)

Estas dos rutas no usan sesión, cookies ni la protección de origen: la autenticación viaja en la propia petición. Solo admiten POST (405 con Allow para lo demás).

Método Ruta Acceso Qué hace
POST /v1/ci/github-actions/token Token OIDC de Actions Canjea {"token": "<id_token>"} por una credencial de bot de tipo ci según la regla de confianza que coincida. Responde 201 con credential, token, kubeconfig, clusters y expires_at, una sola vez. Límite: 60 peticiones por minuto e IP. Ver Credenciales para GitHub Actions.
POST /v1/github/webhook Firma de GitHub Recibe los eventos de la GitHub App. organization/member_removed deshabilita al momento la cuenta vinculada; installation/deleted o suspend se registran en el log; el resto se acepta y se ignora. Exige Content-Type: application/json y un cuerpo de hasta 1 MiB. Sin GITHUB_WEBHOOK_SECRET responde 404.

MCP e inicio de sesión de agentes (fuera de /api)

Aquí no se aplica nada de la sesión, las cookies ni la protección de origen de /api. Existen mientras KUBELATCH_MCP_ENABLED es true; si no, 404. Servidor MCP las describe enteras.

Método Ruta Acceso Qué hace
POST /mcp klt_ de bot o token de agente El servidor MCP (Streamable HTTP, un mensaje JSON-RPC por petición). Cada 401 lleva WWW-Authenticate: Bearer resource_metadata="…".
GET /.well-known/oauth-protected-resource/mcp Anónimo Metadatos del recurso protegido (RFC 9728).
GET /.well-known/oauth-authorization-server Anónimo Metadatos del servidor de autorización (RFC 8414).
POST /oauth/register Anónimo Registro dinámico de clientes (RFC 7591). 20 por minuto e IP.
GET /oauth/authorize Anónimo Empieza un inicio de sesión y lleva el navegador a la página de consentimiento. 30 por minuto e IP.
POST /oauth/token Anónimo Canje del código y refresco. 120 por minuto e IP, contadas junto con /oauth/revoke.
POST /oauth/revoke Anónimo Revocación de tokens (RFC 7009). Cuenta para las mismas 120 por minuto e IP que /oauth/token.

Cualquier otra ruta bajo /.well-known/ u /oauth/ es un 404 en JSON.

Sesión y cuenta propia

Método Ruta Acceso Qué hace
GET /api/auth/methods Anónimo Qué ofrece la página de login, siempre 200: {"password": false, "github": true, "org": "acme", "oidc": true, "oidc_name": "Google", "oidc_icon": "google", "access_contact": "mailto:it@acme.com"}. password es true solo mientras no está activo ni el login con GitHub ni el login con OIDC; oidc_name es OIDC_DISPLAY_NAME, "" sin OIDC; oidc_icon es el logo de su botón (google, microsoft, okta, auth0 o generic), de OIDC_ICON o deducido de OIDC_ISSUER, "" sin OIDC; access_contact es KUBELATCH_ACCESS_CONTACT, o ""; join es true con el login con OIDC activo, cuando alguien sin cuenta puede entrar y pedir unirse.
POST /api/auth/login Anónimo Login con contraseña: {"login", "password"}. Abre sesión y devuelve la cuenta.
POST /api/auth/logout Sesión Cierra todas las sesiones de la persona. 204.
POST /api/auth/link Anónimo Consulta un enlace de invitación o vinculación sin consumirlo: {"token": "kli_…"} → login, display_name, purpose, expires_at, password_allowed (la cuenta puede usar contraseña) y los logins externos activos, que ofrece la página del enlace: github, oidc y oidc_name.
POST /api/auth/link/complete Anónimo Completa un enlace fijando la contraseña: {"token", "password"}. Abre sesión.
GET /api/me Sesión, token de la CLI La cuenta propia: login, nombre, email, is_admin, si tiene y puede usar contraseña, cuenta break-glass, vínculo con GitHub, oidc (su identidad del proveedor OIDC configurado: issuer, subject, email y linked_at, o null), locale (el idioma de la interfaz, null hasta que la persona elige), edition (free o pro), features (las funciones de la edición en vigor: ["agent_insights", "webhooks", "environment_rules", "cluster_sets", "access_requests", "scope_owners"] en Pro, donde agent_insights enseña lo que hizo cada agente en el último día y cuáles merecen un vistazo, webhooks son los webhooks de los avisos, environment_rules es poner y cambiar las reglas de un entorno, cluster_sets conceder y ampliar permisos sobre un conjunto y access_requests pedir acceso, y el enlace que trae un 403 del cluster (403 — sin permiso), y scope_owners nombrar a los dueños de un entorno, un cluster, un namespace o un grupo, y que actúen como tales; [] en Free; los enlaces por correo, el antiguo email_links, son Free) y mail_configured (el correo está configurado en el servidor; siempre false para quien no es admin), notify_grant_email (si recibes el correo antes de que caduque uno de tus permisos; lo cambia PATCH /api/me/notifications), provider_logos (qué logos de proveedor dibuja la interfaz: published, all o none, como fije KUBELATCH_PROVIDER_LOGOS) y last_activity_at (cuándo pasó por el proxy la última petición de la persona, null antes de la primera). notice_email es adónde van tus avisos, "" si a ninguna parte, y notice_email_source de dónde sale: account (tu correo), oidc (el correo de tu identidad OIDC, si no tienes uno propio) o "" (El correo de la cuenta). oidc lleva además linked_via: email, link, me, domain o join. decides es true mientras una solicitud de acceso espera tu decisión. owns lista los ámbitos de los que eres dueño ahora, tú o por un grupo, como el scope de un dueño; [] sin ninguno y sin scope_owners. Con un token de la CLI, is_admin y decides son siempre false y owns es [].
PATCH /api/me/notifications Sesión Enciende o apaga el correo antes de que caduque uno de tus permisos (Avisos): {"grant_email": false} → el me actualizado, cuyo notify_grant_email dice el valor. 400 request.bad_json sin un grant_email booleano.
GET /api/me/notices Sesión Los avisos de la campanita para ti: {"notices": [...]}, cada uno con un kind. grant_expiring: {"kind": "grant_expiring", "grant_id", "cluster", "cluster_name", "role", "scope", "expires_at", "direct"}, tus permisos activos que terminan en menos de 24 horas, el más próximo primero; direct es true para un permiso propio y false para uno que te llega por un grupo o un conjunto, que una solicitud no puede ampliar. Después, access_request_decided: {"kind": "access_request_decided", "request_id", "status", "cluster", "cluster_name", "role", "scope", "decided_by": {"id", "login", "name"}, "granted_until", "note", "at"}, una por cada solicitud de acceso tuya que una persona aprobó o denegó en las últimas 24 horas, la decisión más reciente primero (granted_until es null en una denegación, note es lo que te escribió quien decidió y at cuándo lo hizo); la aprobación por la regla de un entorno, la cancelación y la caducidad no tienen línea. Luego access_requests_waiting: {"kind": "access_requests_waiting", "count"}, cuántas solicitudes esperan ahora tu decisión, mientras haya alguna. Luego, para un administrador, break_glass: {"kind": "break_glass", "subject_id", "login", "at", "until"}, uno por cuenta break-glass, durante 12 horas desde su último acceso con contraseña (until es at más 12 horas). Luego, para un administrador fuera de Pro con webhooks activos, webhooks_paused: {"kind": "webhooks_paused", "count"}, cuántos de ellos no entregan nada (Cuando termina Pro). Luego, para un administrador en Pro, agent_worth_a_look: {"kind": "agent_worth_a_look", "session_id", "agent": {"mode", "client_name", "subject_login"}, "look", "count", "at"}, uno por agente con una sesión activa que merece un vistazo, el más reciente primero. look (flood, sensitive, insistence o denied), count y at son el motivo más reciente del agente, tal como lo lista worth_a_look en GET /api/agents/sessions, y session_id es la sesión activa del agente usada más recientemente, la que abre la línea. Se calcula en cada lectura y desaparece con el motivo; en Free faltan solo las de Pro, y quien no es admin no recibe ninguna de estas (Avisos).
PATCH /api/me/locale Sesión Guarda tu idioma de la interfaz: {"locale": "en"} o {"locale": "es"} → el me actualizado. 400 locale.invalid con cualquier otro valor.
POST /api/me/password Sesión Cambia la contraseña propia: {"current_password", "new_password"}. Cierra las demás sesiones.

Login con GitHub

Método Ruta Acceso Qué hace
GET /api/auth/github/login Anónimo Fija la cookie de estado y redirige (302) a GitHub para autorizar la App. El ?next= opcional es la página a la que vuelve el callback: solo una ruta del mismo origen; cualquier otra cosa vuelve a /.
GET /api/auth/github/callback Anónimo con cookie de estado Vuelta desde GitHub. Redirige con sesión al next del login (o a /; siempre a / tras un enlace de invitación o de vinculación), a /account?github=linked tras vincular desde «Mi cuenta», o a /login?error=<código> (/account?error=<código>) si falla.
POST /api/auth/github/link Anónimo Empieza a completar un enlace de invitación o vinculación con GitHub: {"token": "kli_…"} → {"url"} a la que navegar.
POST /api/me/github/link Sesión Empieza a vincular la cuenta propia, si aún no está vinculada (409 si ya lo está) → {"url"}.
GET /api/github/status Admin Configuración (configured, org, webhook, require_org_2fa) y la última sincronización de miembros (last_sync: at, los contadores members, linked, disabled y renamed, skipped, two_factor_required y error, un código de causa: list_members, no_members, database o disable_failed, vacío si fue bien). Ausente hasta la primera sincronización.

Sin GitHub configurado, /api/auth/github/login, /api/auth/github/callback, /api/auth/github/link y /api/me/github/link responden 404.

Login con OIDC

Las rutas del login con OIDC. Sin OIDC configurado, todas salvo /api/oidc/status responden 404 oidc.disabled.

Método Ruta Acceso Qué hace
GET /api/auth/oidc/login Anónimo Fija la cookie de estado y redirige (302) al endpoint de autorización del proveedor. El ?next= opcional es la página a la que vuelve el callback: solo una ruta del mismo origen; cualquier otra cosa vuelve a /. ?select=1 pide al proveedor su selector de cuentas. Si no se puede leer el documento de descubrimiento del proveedor, redirige (303) a /login?error=provider_unavailable&via=oidc. Es también la URL que va en el icono de la aplicación del lanzador del proveedor, porque kubelatch no acepta accesos iniciados por el proveedor (Sin inicio de sesión desde el proveedor).
GET /api/auth/oidc/callback Anónimo con cookie de estado Vuelta desde el proveedor; siempre borra la cookie de estado. Redirige (303) con sesión al next del login (o a /; siempre a / tras un enlace de invitación o de restablecimiento), a /account?oidc=linked tras vincular desde «Mi cuenta», o a /login?error=<código>&via=oidc (/account?error=<código>&via=oidc) si falla: los códigos están en Errores. Un acceso cuyo correo no tiene ninguna cuenta, y al que ninguna regla de dominio da de alta, va a /join, con la cookie de unión (Incorporación).
POST /api/auth/oidc/link Anónimo Empieza a completar un enlace de invitación o vinculación con el proveedor: {"token": "kli_…"} → {"url"} a la que navegar, con la cookie de estado. Con el mismo límite por IP que POST /api/auth/link. Además de los errores del enlace (404 link.invalid, 410 link.gone), 503 oidc.unavailable mientras no se puede leer el documento de descubrimiento.
POST /api/me/oidc/link Sesión Empieza a vincular la cuenta propia → {"url"}, con la cookie de estado. 409 oidc.already_linked si la cuenta ya tiene una identidad del proveedor configurado; 503 oidc.unavailable mientras no se puede leer el documento de descubrimiento.
GET /api/oidc/status Admin Siempre 200. Sin OIDC, {"configured": false}. Con OIDC: issuer, name, domains, autolink, subject_claim, email_verified_claim, public_client, discovery (fetched_at, null hasta la primera lectura correcta, y error, la última lectura fallida, "" en cuanto una posterior va bien), jwks_fetched_at (null hasta la primera lectura correcta) recent_email_links, las cuentas vinculadas solas en los últimos siete días, de la más reciente a la más antigua, como mucho 50 (subject_id, login, display_name, email, linked_at y linked_via: email, domain en un alta por dominio o join en una solicitud aprobada), y signup_domains, cuántas reglas de dominio hay. Cada réplica responde con sus propios discovery y jwks_fetched_at.

Incorporación

Acceso por dominio y solicitudes de alta. /api/join no tiene sesión: lo único que prueba quién pide es la cookie de unión (__Host-kubelatch_join sobre https, kubelatch_join sobre http sin cifrar, diez minutos), que el callback de OIDC deja a un acceso cuyo correo no tiene ninguna cuenta. Sus dos métodos comparten un límite por IP propio, de 30 por minuto y aparte del del login (429 auth.too_many_attempts). El resto pide el rol de administrador. Sin OIDC configurado, /api/join responde 401 join.no_identity.

Las solicitudes de alta comparten /api/access-requests con las solicitudes de acceso de quien ya tiene cuenta. Una solicitud de alta solo existe para un administrador: cualquier otra persona que la nombre recibe 404 access_request.not_found, igual que con un id desconocido o mal formado.

Método Ruta Acceso Qué hace
GET /api/join Cookie de unión Cómo está la identidad de la cookie: {"status", "provider_name", "email", "name", "reason", "requested_at", "expires_at", "decided_at", "access_contact"}. status es new, pending, denied, expired, approved o cancelled; reason dice por qué el callback no dio de alta a nadie (none, seat_limit o login_taken); las fechas que un estado no tiene van a null. La nota del administrador nunca aparece. 401 join.no_identity sin una cookie válida, con una ya usada o cuando una cuenta ya tiene la identidad.
POST /api/join Cookie de unión Pide unirse: {"reason"}, opcional, hasta 500 caracteres sin caracteres de control → 201 con {"status": "pending", "requested_at", "expires_at"}, 30 días después. Gasta la cookie. 409 join.pending si la identidad ya tiene una solicitud pendiente, 429 join.too_many con 100 pendientes, 400 join.bad_reason, 401 join.no_identity.
GET /api/access-requests Admin Sin view, un array de solicitudes de alta: ?status=pending (por defecto), de la más antigua a la más reciente, o ?status=decided, de la más reciente a la más antigua, como mucho 200 (si no, 400 query.bad_request_status); ?kind=join&view=waiting y ?kind=join&view=decided son esas mismas dos listas. Cada una: id, kind (join), status, email, name, issuer, reason, created_at, expires_at, suggested_login (en una pendiente, el primer login que da el correo y que no tiene ninguna cuenta; "" si los dos existen), decided_by_login, decided_at, decision_note y result_subject_id (null o vacíos mientras está pendiente).
POST /api/access-requests/{id}/approve Admin Crea la cuenta, vincula su identidad y le da su primer acceso, todo o nada: {"login", "access": {"kind": "group", "group_id"}}, {"login", "access": {"kind": "grant", "grant": {"cluster_id", "role", "scope", "expires_at", "never_expires", "note", "ticket"}}} (la caducidad, como en POST /api/grants) o {"login", "access": {"kind": "none"}} → la cuenta nueva, como GET /api/subjects/{id}. 400 join.bad_access, 400 subject.invalid_login, 409 subject.login_taken, 409 join.email_taken (una cuenta ya tiene ese correo o esa identidad), 409 join.decided, 410 join.expired, 404 access_request.not_found, 422 edition.limit, y los errores propios del grupo o del permiso tal como los responden sus rutas.
POST /api/access-requests/{id}/deny Admin La deniega: {"note"}, una nota interna de hasta 500 caracteres (400 join.bad_note) → la solicitud como la muestra la lista. 409 join.decided, 410 join.expired, 404 access_request.not_found.
GET /api/signup-domains Admin Un array con las reglas de dominio: domain, group_id, group_name, member_ttl_seconds (null para una pertenencia sin caducidad), allowed (false cuando el dominio ha salido de OIDC_ALLOWED_DOMAINS: la regla no hace nada), signups (las cuentas que ha creado), members (sus pertenencias vivas), created_by_login y created_at.
POST /api/signup-domains Admin Crea una regla: {"domain", "group_id", "member_ttl_seconds"}, o con un grupo nuevo, {"domain", "new_group": {"id", "name", "grant": {"cluster_id", "role", "scope", "expires_at", "never_expires", "note", "ticket"}}, "member_ttl_seconds"} (grant opcional, con la caducidad, note y ticket como en POST /api/grants: 400 grant.reason_required o grant.ticket_required cuando el entorno del cluster los pide), con el grupo, su permiso y la regla en una sola transacción → 201 con la regla. 422 signup_domain.not_allowed para un dominio fuera de OIDC_ALLOWED_DOMAINS, 409 signup_domain.exists, 400 signup_domain.bad_group sin exactamente uno de group_id y new_group, 400 signup_domain.bad_ttl fuera de 1 minuto a 3650 días, 404 group.not_found, y los errores propios del grupo.
PATCH /api/signup-domains/{domain} Admin Cambia la caducidad de la pertenencia de quienes la regla dé de alta a partir de ahora: {"member_ttl_seconds": 2592000}, obligatorio, null para sin caducidad → la regla. 404 signup_domain.not_found, 400 signup_domain.bad_ttl.
DELETE /api/signup-domains/{domain} Admin Quita la regla y las pertenencias vivas que añadió, con sus permisos derivados → 204. Las cuentas siguen habilitadas. 404 signup_domain.not_found.

Usuarios y bots

Todas requieren admin.

Método Ruta Qué hace
GET /api/subjects Lista personas y bots. Cada uno lleva access: {"groups", "direct"}, los grupos activos a los que pertenece y sus permisos directos activos; link_expires_at, cuándo caduca su enlace de invitación o de restablecimiento pendiente (null si no hay); last_activity_at, lo más reciente entre su último inicio de sesión, su último uso de una credencial y su última petición auditada (null si nunca hizo nada); pending_link_delivery, cómo salió su enlace pendiente, link o email (null si no hay); y oidc, su identidad del proveedor OIDC configurado (issuer, subject, email, linked_at y linked_via, cómo se vinculó: email, link, me, domain o join; null si no tiene, o sin OIDC). notice_email y notice_email_source dicen adónde van sus avisos, como en GET /api/me. Las demás respuestas que devuelven un sujeto llevan access a cero y link_expires_at, last_activity_at, pending_link_delivery y oidc a null, y un notice_email que es el correo propio de la cuenta, salvo la de PATCH /api/subjects/{id}, que también mira la identidad.
GET /api/subjects/{id} Una persona o un bot, con los mismos access, link_expires_at, last_activity_at, pending_link_delivery y oidc que la lista, más github_linked_at, cuándo vinculó su cuenta de GitHub, y password_changed_at, cuándo cambió su contraseña por última vez (null si nunca). 404 subject.not_found si el id no existe o no es un uuid.
POST /api/subjects Crea una persona ({"login", "display_name", "email", "is_admin", "delivery"}) y devuelve subject y su invite_link una sola vez, con link_expires_at, cuándo caduca ese enlace (RFC 3339). Con "delivery": "email" el enlace va al correo de la cuenta, y la respuesta lleva delivered_to en lugar de invite_link: sus errores están en Invitaciones por correo. 409 subject.email_taken si otra cuenta tiene ese correo. O crea un bot ({"kind": "bot", "login", "display_name"}) → subject.
PATCH /api/subjects/{id} Cambia display_name, email o is_admin de una persona → la cuenta, con revoked_links, cuántos enlaces que salieron por correo anuló el cambio de correo (ausente si son 0). Un email nuevo es una sola dirección o "" para quitarlo (400 subject.email_invalid), no es de otra cuenta (409 subject.email_taken) y, cuando un primer acceso OIDC con él podría abrir la cuenta, ahora o tras una desvinculación, necesita "confirm_autolink": true (409 subject.email_autolinks, con params.domain): ver El correo de la cuenta. En un bot, solo {"agent_writes_need_approval": true\|false}, el interruptor de sus agentes de IA (un campo de perfil en un bot es 409; el interruptor en una persona, 400 subject.not_bot).
POST /api/subjects/{id}/reset-link Genera un enlace de restablecimiento o de vinculación (24 h) y lo devuelve una vez como reset_link, con link_expires_at, cuándo caduca (RFC 3339). Con el cuerpo opcional {"delivery": "email"} envía el enlace por correo y responde delivered_to en lugar de reset_link. Revoca los pendientes y cierra al momento las sesiones de esa persona. 409 si la cuenta está deshabilitada.
POST /api/subjects/{id}/disable Deshabilita a una persona o un bot: cierra sesiones y revoca credenciales. Una cuenta ya deshabilitada se responde tal como está.
POST /api/subjects/disable Deshabilita de 1 a 500 personas o bots a la vez: {"ids": [...], "reason": ""} → 200 con {"batch_id", "subjects": [...], "unchanged": [...]}, como en operaciones en lote. Cada cuenta se deshabilita como con el disable individual: sus sesiones, credenciales y sesiones de agente terminan al momento. subjects reúne las cuentas deshabilitadas, como las enseña la lista. 409 subject.last_admin, con params.index y params.id, para la cuenta que dejaría sin ningún administrador que pueda entrar, contando las que el mismo lote deshabilita antes que ella: no se deshabilita ninguna. 404 subject.not_found para un id que no es una cuenta. Una cuenta ya deshabilitada vuelve en unchanged.
POST /api/subjects/{id}/enable Rehabilita. Las credenciales revocadas siguen revocadas.
POST /api/subjects/{id}/logout Cierra todas las sesiones de una persona.
DELETE /api/subjects/{id}/github Desvincula la cuenta de GitHub de una persona y cierra sus sesiones.
DELETE /api/subjects/{id}/oidc Desvincula la identidad OIDC de una persona y cierra sus sesiones → la cuenta. Sin identidad, responde 200 y no cambia nada. 409 subject.last_admin con el último administrador que puede entrar, 409 subject.not_user con un bot, 404 oidc.disabled sin OIDC configurado.
POST /api/mail/test Te envía el correo de prueba a tu propio notice_email, sin cuerpo (un campo es 400 request.bad_json) → {"to"}. 409 mail.not_configured, 422 mail.no_address, 429 mail.rate_limited (5 por hora, de uno en uno), 502 mail.test_failed con params.status (el de Resend, "0" si no respondió) y params.name (el nombre del error de Resend, "" si se desconoce).

Clusters

Todas requieren admin, salvo GET /api/clusters/{id}/namespaces, que lee también el dueño del cluster entero.

Método Ruta Qué hace
GET /api/clusters Lista los clusters con estado, última reconciliación, último error, last_error_at (cuándo ocurrió ese error; null salvo que el estado sea error), bootstrap_version (1 o 2: el manifiesto de bootstrap que encontró la última reconciliación; los roles personalizados y los nativos necesitan 2), user_template, la plantilla de identidad del cluster (user:{login} por defecto), y unnamed_logins, las cuentas con un permiso activo que la plantilla no pudo nombrar en la última reconciliación (una lista, [] si las nombró a todas): es un aviso, no un error, porque el cluster sigue en ready y esas cuentas reciben un 403 hasta que tengan el dato de identidad que pide la plantilla. Cada cluster lleva además su proveedor: provider, el que se enseña (eks, gke, aks, doks, openshift, k3s, rke2, kapsule, sks, ovh-mks, aruba-mks, talos, kind o kubernetes: el fijado a mano, si no el deducido, y si no kubernetes), provider_detected, lo que dedujo la última reconciliación ("" hasta la primera; nunca kind) y provider_manual, true cuando provider se fijó a mano. Dónde está: environment, el id de su entorno (null si no está en ninguno), production, si ese entorno está marcado como producción (false sin entorno), y labels, sus etiquetas como un objeto ({} sin ninguna, nunca null). Tres booleanos dicen lo que necesita la salud del cluster: k8s_unsupported (su versión de Kubernetes está fuera de soporte), everyone_privileged (el cluster da acceso privilegiado a todo el que autentica) y pass_overdue (está enrolado y su última reconciliación, buena o fallida, tiene más de 30 minutos). Por último, problems: lo que la última reconciliación fallida encontró mal, una entrada por problema y ninguna recortada ([] si no hay ninguno, nunca null; last_error guarda las mismas líneas, recortadas a 1000 caracteres). Cada una lleva code y text, la línea en inglés; el resto depende del code: privileged (un nombre al que el cluster da acceso privilegiado y que un permiso no puede dar: name, name_kind group o user, reason como en rbac-subjects, culprits, victims, y more_culprits y more_victims cuando una lista se corta en 100), collision (name, logins), bootstrap_incomplete, bootstrap_outdated (what: custom o native), role_refused (role_id), identity_template (template), apply_failed (object: {"verb", "kind", "name", "namespace", "status"}, con status el estado HTTP del cluster o 0), reconciler_client, unreachable (el reconciler no llegó al API server: cause es network para una conexión sin respuesta y unauthorized para un 401 a su token) y generic (solo text). Un culpable es {"kind", "id", …}: un grant (login, role, scope), un group_grant, el permiso de un grupo (group_id, group_name, role, scope) o un agent_session (login). victims son los ids de las cuentas a las que la negativa deja sin acceso al cluster.
POST /api/clusters Registra un cluster: {"id", "name"}. El id es el slug de la URL del proxy.
GET /api/clusters/{id} Un cluster, con los mismos campos que la lista, last_error_at y problems incluidos.
PATCH /api/clusters/{id} Guarda la plantilla de identidad y el proveedor: {"user_template": "oidc:{email}", "provider": "k3s"} → el cluster. Admite estos dos campos, cada uno opcional, y al menos uno; cualquier otro, o ninguno, responde 400. Los dos se comprueban primero y se guardan juntos: si se rechaza uno, no se guarda nada. Una plantilla no válida responde 400 con su código (cluster.template_variables, cluster.template_bad_literal, cluster.template_reserved_prefix, cluster.template_too_long), y {email} u {oidc_subject} sin el login con OIDC configurado, 409 cluster.template_needs_oidc. Si otro cambio tiene retenido el cluster más de dos segundos, 409 cluster.busy: inténtalo de nuevo. La plantilla registra cluster.identity.update y reconcilia el cluster. provider es uno de eks, gke, aks, doks, openshift, k3s, rke2, kapsule, sks, ovh-mks, aruba-mks, talos, kind o kubernetes y fija el proveedor a mano; "" lo devuelve a la deducción y omitirlo no cambia nada; cualquier otro valor responde 400 cluster.provider_invalid. Registra cluster.provider.update (before y after, "" para automático) solo cuando el valor cambia, y no inicia ninguna reconciliación.
PUT /api/clusters/{id}/placement Fija a la vez el entorno y las etiquetas del cluster: {"environment": "prod", "labels": {"equipo": "pagos", "region": "eu"}} → el cluster, con cut, lost y gained como en PATCH /api/environments/{id}. Sustituye las dos cosas: environment es el id de un entorno, o null para ninguno, y labels todas sus etiquetas, {} para ninguna. Los dos campos son obligatorios: un cuerpo al que le falte uno, que traiga otro campo o un valor de otro tipo responde 400 request.bad_json y no cambia nada. Una etiqueta con mal formato responde 400 cluster.label_invalid, con su clave en params.key; la clave environment o entorno, 400 cluster.label_reserved; más de 20 etiquetas, 400 cluster.labels_too_many. Un entorno que no existe es 400 environment.not_found, y un cluster que no existe, 404 cluster.not_found. Si otro cambio tiene retenido el cluster o el entorno más de dos segundos, 409 cluster.busy: no se escribe nada; inténtalo de nuevo. Un base opcional, con los mismos dos campos, es la colocación de la que parte el cambio: si el cluster tiene otra, porque alguien la cambió después de leerla, la respuesta es 409 cluster.placement_changed y no se escribe nada; la interfaz web lo envía siempre. Registra cluster.placement (from y to, cada uno con environment y labels, cut, cuántos permisos recortó, y lost y gained, los permisos sobre un conjunto que dejaron o empezaron de llegar al cluster) aunque no cambie nada. Si mete el cluster en un entorno con duración máxima, recorta en el cluster lo que la supera, como PATCH /api/environments/{id}; los permisos sobre un conjunto que siguen al entorno o a las etiquetas a los que entra llegan al cluster, y los de los que deja dejan de llegar. Reconcilia el cluster cuando algo de esto cambia un permiso; cualquier otra colocación no cambia ninguno ni inicia ninguna reconciliación.
POST /api/clusters/{id}/placement/preview Lo que recortaría PUT /api/clusters/{id}/placement con {"environment", "labels"} (los dos obligatorios, sin base), sin escribir nada: {"cut", "lost", "gained"}, como en POST /api/environments/{id}/preview. Rechaza lo mismo que el PUT, con los mismos códigos.
GET /api/cluster-labels Las claves de etiqueta en uso, cada una con sus valores: {"labels": [{"key": "region", "values": ["eu", "us"]}]}, claves y valores ordenados y sin repetir. Es lo que ofrecen «Agrupar por» y el filtro «Etiqueta».
DELETE /api/clusters/{id} Limpia el RBAC que kubelatch gestiona en el cluster y elimina el cluster, sus permisos y sus credenciales restringidas. Devuelve rbac_cleanup con status (ok, error o skipped), error, bindings_deleted, roles_deleted y lock_held.
GET /api/clusters/{id}/bootstrap.yaml Descarga el manifiesto de bootstrap para aplicarlo en el cluster.
POST /api/clusters/{id}/tokens Pega el JSON del bootstrap: {"server", "ca", "proxyToken", "reconcilerToken"}. Valida, cifra y lanza la primera reconciliación.
POST /api/clusters/{id}/enrollment Crea un enlace de enrolamiento de un solo uso, válido una hora: {"api_url"?, "in_cluster"?, "rotate"?} → 201 con {"id", "command", "expires_at"}. El token kle_ solo aparece dentro de command, una vez. in_cluster fija https://kubernetes.default.svc y es incompatible con api_url (400 enrollment.url_conflict). Un enlace nuevo sustituye al anterior.
GET /api/clusters/{id}/enrollment El último enrolamiento del cluster: {"id"?, "state": "none"\|"pending"\|"running"\|"used"\|"expired"\|"revoked", "expires_at"?, "last_failure", "method", "in_cluster_ca", "apiserver_user"?, "apiserver_groups"?, "api_url"?, "ca"?, "created_by"?}. id falta con el estado none. last_failure es null, o el último envío del script que kubelatch rechazó, o por qué falló un enrolamiento con una identidad: {"at", "code", "params", "reason"}. code y params son la forma codificada del rechazo, que la interfaz traduce (code va vacío en un error interno), y reason es su texto en inglés. method es script o identity (vacío con el estado none); running es un enrolamiento con una identidad cuyo callback está aplicando el manifiesto. in_cluster_ca dice si el servidor tiene la CA del cluster donde se ejecuta. apiserver_user y apiserver_groups son cómo vio el API server a la identidad, en un enrolamiento con una identidad que llegó hasta él. api_url, ca y created_by solo vienen con un enrolamiento con una identidad: adónde apuntaba (ninguno de los dos es secreto), para que un reintento salga relleno, y el id de la cuenta que lo empezó; api_url y ca solo le llegan a esa cuenta. Nunca lleva un token.
DELETE /api/clusters/{id}/enrollment Invalida el enlace vigente, o un enrolamiento con una identidad pendiente, si lo hay. 204 en ambos casos; 409 enrollment.in_progress mientras un enrolamiento con una identidad está en marcha, como responde también POST.
POST /api/clusters/{id}/enrollment/identity Empieza un enrolamiento con tu identidad: {"api_url", "in_cluster", "ca", "rotate"}, y cualquier otro campo da un 400 → 200 con {"url"}, la URL de autorización del proveedor, y la cookie de estado. in_cluster usa https://kubernetes.default.svc y, sin ca, la CA que el servidor tiene montada (400 enrollment.in_cluster_ca_missing si no tiene ninguna). 409 enrollment.identity_unlinked para una cuenta sin identidad del proveedor, 409 enrollment.in_progress, 404 oidc.disabled, 503 oidc.unavailable. El callback aplica el manifiesto y vuelve a /admin/clusters/{id}#enrollment. Solo con una sesión del navegador.
GET /api/clusters/{id}/namespaces Los namespaces del cluster: {"namespaces": [{"name", "psa_enforce"}], "require_psa", "psa_label"}. Un administrador, o un dueño del cluster entero (del cluster o de su entorno); cualquier otra persona recibe 403 session.admin_only, exista el cluster o no.
POST /api/clusters/{id}/reconcile Reconcilia ahora, de forma síncrona. Si falla el lado del cluster, 502 cluster.reconcile_failed con params.count, el número de problemas, que enumera el problems del cluster; 409 si hay otra pasada en curso o faltan tokens.
GET /api/clusters/{id}/api-resources La API del cluster, para componer roles personalizados: {"groups": [{"group", "version", "resources": [{"name", "namespaced", "verbs", "subresources"}]}]}, ordenada por grupo. Se guarda cinco minutos. 502 cluster.unreachable si el cluster no responde.
GET /api/clusters/{id}/rbac-subjects Lo que nombra el RBAC propio del cluster: {"groups": [...], "users": [...], "truncated", "fetched_at", "privileged_namespaces", "everyone_privileged"}. Cada sujeto lleva name, bindings ([{"kind": "RoleBinding"\|"ClusterRoleBinding", "namespace", "name", "role_ref": {"kind", "name"}}]), privileged (el cluster le da una capacidad privilegiada), privilege (por qué: una lista de razones, nunca null), system (empieza por system:) y managed (todos los bindings que lo nombran son de kubelatch). Una razón es {"capability", "namespace", "binding", "role"}, los dos últimos como {"kind", "namespace", "name"}: capability es una de all, escalate, bind, impersonate, nodes_proxy, certificates, admission, persistent_volumes, namespaces, secrets_read, secrets_write, workloads, token_request, impersonate_serviceaccounts y configmaps_write, y un namespace vacío significa todo el cluster, o todos los namespaces si la capacidad es de namespace. Las razones salen ordenadas por binding y después por capacidad. privileged_namespaces lista los namespaces privilegiados, ordenados por nombre y con los protegidos incluidos: [{"namespace", "protected", "subject"?, "reason"?}]; subject (una ServiceAccount, un User o un Group, como {"kind", "namespace", "name"}) y reason solo vienen con los namespaces que hace privilegiados una ServiceAccount privilegiada. everyone_privileged lleva las razones de system:authenticated, system:unauthenticated y system:serviceaccounts, y va vacío cuando el cluster no les da nada privilegiado. La clasificación lee todo el RBAC del cluster; solo se cortan las listas de nombres, ordenadas por nombre, como mucho 500 grupos y 500 usuarios (truncated dice si se cortó), y las ServiceAccounts quedan fuera. Se guarda cinco minutos; ?refresh=1 (o true) vuelve a leer el cluster, y cualquier otro valor salvo 0 o false es 400 query.bad_filter. 502 cluster.unreachable si el cluster no responde, 409 cluster.not_ready antes de enrolarlo.
GET /api/clusters/{id}/identity-preview Rinde ?template= (la plantilla guardada si falta) para cada persona con un permiso activo en el cluster, sin escribir nada: {"rows": [{"login", "display_name", "username", "missing", "privileged"}], "collisions": [{"username", "logins"}], "reachable"}. username es null cuando la cuenta no se puede nombrar, y entonces missing dice por qué: la variable que le falta (email, oidc_subject, github_login) o invalid. privileged es true cuando el cluster da acceso privilegiado a ese nombre. Un cluster que no se puede leer responde igualmente 200, con reachable: false y sin marcas de privileged. Una plantilla no válida responde como en PATCH.
GET /api/clusters/{id}/group-refusals Los roles nativos que un permiso de grupo no puede recibir en el cluster, para saberlo antes de elegir un rol: {"reachable": true, "refusals": [{"role", "groups", "reasons"}]}. Un rol sale cuando se puede conceder en el cluster y el cluster da acceso privilegiado a uno de sus grupos; groups son esos grupos, reasons da la primera razón de cada uno como hace /api/grants/preview ({"kind": "group_privileged", "group", "privilege"}), y un permiso de grupo de ese rol se rechaza con 422 group.privileged, la misma regla que /api/grants/preview. Ordenado por id de rol; ninguna de las dos listas es null. Lee el RBAC del cluster como la vista previa, guardando lo leído cinco segundos, y solo cuando se puede conceder algún rol nativo ahí. Un cluster que no se puede leer responde igualmente 200, con reachable: false y sin rechazos: entonces no se sabe cuáles rechaza. 404 cluster.not_found. No escribe nada.

Enrolamiento

El comando de enrolamiento llama a estas dos rutas. El token kle_ de la ruta es la única credencial.

Método Ruta Acceso Qué hace
GET /api/enroll/{token} Anónimo El script de enrolamiento para sh; no consume el enlace. Un enlace desconocido, usado, caducado o invalidado recibe 404 con un script que imprime el error.
POST /api/enroll/{token} Anónimo El mismo JSON que /tokens. Valida y guarda los tokens, y solo consume el enlace si todo va bien; responde {"id", "name", "status"}. Una validación fallida deja el enlace utilizable, para repetir el comando; el décimo fallo lo revoca. 404 enrollment.invalid para un enlace que ya no sirve.

Entornos

Todas requieren admin. Un entorno reúne clusters bajo un nombre, dice si son de producción y lleva las reglas de los permisos de las personas en ellos; un cluster está en uno como mucho, y se fija con PUT /api/clusters/{id}/placement.

Las reglas son max_ttl_seconds, la duración máxima, y default_ttl_seconds, la caducidad por defecto: cada una, un número entero de segundos de 3600 a 31536000, o null si no la hay (400 environment.ttl_invalid, con la regla en params.field), y la caducidad por defecto nunca más larga que el máximo (400 environment.default_over_max). Poner una regla en un valor que no tiene pide Pro, 403 edition.feature; quitar una, dejarlas como están y la marca de producción funcionan en cualquier edición.

Método Ruta Qué hace
GET /api/environments {"environments": [...], "unassigned": 2}: todos los entornos, primero los marcados como producción y después por id, cada uno con id, production, clusters (cuántos tiene), created_at, max_ttl_seconds, default_ttl_seconds, require_reason, require_ticket y auto_approve_roles; unassigned cuenta los clusters que no están en ninguno.
POST /api/environments Crea uno: {"id": "prod", "production": true, "max_ttl_seconds": 28800, "default_ttl_seconds": 7200} → 201 con él. El id es su nombre y no cambia nunca: de 1 a 63 letras minúsculas, dígitos o guiones, empezando por una letra o un dígito (400 environment.bad_id). production y las reglas son opcionales: false y ninguna por defecto. 409 environment.exists si otro entorno tiene ese id. Registra environment.create con la marca y las reglas.
GET /api/environments/{id} {"environment", "clusters", "permissions"}: el entorno, sus clusters como los da GET /api/clusters, y permissions, {"grants", "people", "via_sets"}: los permisos activos de cuentas habilitadas en esos clusters, cuántas personas (no bots) tienen alguno y cuántos de esos permisos llegan por un conjunto. 404 environment.not_found.
PATCH /api/environments/{id} Cambia la marca, las reglas, los requisitos, los roles que se conceden sin aprobación, o varios: {"production": true}, {"rules": {"max_ttl_seconds": 28800, "default_ttl_seconds": null}}, {"requirements": {"require_reason": true, "require_ticket": false}} o {"auto_approve_roles": ["viewer", "developer"]}, al menos uno. production es un booleano; rules lleva las dos claves y sustituye las dos reglas; requirements lleva las dos claves; auto_approve_roles es la lista completa de ids de rol que una solicitud de acceso recibe sin aprobación ([] para ninguno, se guarda ordenada), cada uno un tier que no sea cluster-admin o un rol personalizado, sin repetidos y como mucho 50 (400 environment.auto_approve_role, con el rol en params.role); añadir un rol necesita Pro (403 edition.feature), quitarlo nunca, y la lista no recorta nada. Cualquier otro cuerpo es 400 request.bad_json. Con un máximo, recorta hasta ahora más el máximo cada permiso activo de una persona o de un grupo en sus clusters que termina después o nunca, nunca el propio de un bot, y reconcilia cada cluster que toca; no alarga nada. → el entorno, cut y, para los permisos sobre un conjunto, lost y gained. cut es {"to", "grants", "people", "items", "truncated"}: to es ahora más el máximo (null sin máximo), grants cuántos permisos recortó (los propios de las personas, los de grupo y lo que un permiso sobre un conjunto puso en un cluster; las filas de los miembros de un grupo siguen a las suyas), people a cuántas personas llegan, items los 500 primeros por cluster, cada uno {"kind", "id", "cluster_id", "subject_id", "login", "subject_disabled", "group_id", "group_name", "role", "scope", "from"} (kind es grant para el permiso propio de una persona, group_grant para el de un grupo y set_row para lo que un conjunto puso en el cluster, que lleva además set_grant_id y selector; from es cuándo terminaba antes, null si no terminaba), y truncated si hay más. lost y gained listan lo que el guardado quitó a los clusters y lo que les dio, como lost y gained en POST /api/cluster-set-grants/{id}/preview: relajar una regla puede traer un permiso sobre una etiqueta que el máximo rechazaba. Si otro cambio tiene retenido lo que cambia más de dos segundos, 409 cluster.busy: no se escribe nada. 404 environment.not_found. Registra environment.update aunque no cambie nada; cada permiso recortado escribe su grant.expiry o su group.grant.update, todos con un mismo batch.
POST /api/environments/{id}/preview Lo que haría PATCH con el mismo cuerpo, sin escribir nada y sin pedir Pro: {"cut", "lost", "gained"}, cada uno como en la respuesta del PATCH. Con {"delete": true} solo, dice lo que haría DELETE: nada que recortar, los permisos sobre un conjunto que siguen al entorno, que revocaría, en lost, y en gained lo que ganarían sus clusters; cualquier otra cosa junto a delete es 400 request.bad_json. Rechaza el cuerpo y las reglas como el PATCH; 404 environment.not_found.
DELETE /api/environments/{id} Lo elimina → 204. Sus clusters se quedan sin entorno y conservan sus etiquetas; los permisos sobre un conjunto que lo siguen se revocan, y los clusters ganan lo que sus reglas les impedían. Si otro cambio tiene retenido alguno de ellos más de dos segundos, 409 cluster.busy: no se elimina nada; inténtalo de nuevo. 404 environment.not_found. Registra environment.delete con los clusters que deja sin entorno, y lost y gained.

Permisos

Un administrador escribe todos los permisos. En Pro, los dueños de un sitio también conceden, amplían, previsualizan y revocan ahí los permisos de personas y bots, dentro de sus límites: fuera de lo que gestionan, 403 owner.out_of_scope (con params.index, y params.id donde la ruta toma ids, en un lote); darse acceso a sí mismos, 403 owner.self; y lo que solo concede o cambia un administrador (cluster-admin, un rol nativo, un permiso que el cluster hace privilegiado, dar acceso a un bot), 403 owner.admin_only. Cualquier otra persona recibe 403 session.admin_only antes de que se lea el cuerpo. Sus eventos llevan "as": "owner" y owner_scope.

Método Ruta Acceso Qué hace
GET /api/grants Sesión Lista permisos. Filtros: subject, cluster, include_revoked=true, y active=true para solo los activos ahora (ni revocados ni caducados; include_revoked deja entonces de influir). Sin rol de admin, solo los propios (403 si pides otro sujeto); el created_by_login de cada permiso solo se rellena para un admin, vacío en caso contrario. managed=1, sin rol de admin, lista en su lugar los permisos de los sitios de los que eres dueño, sean de quien sean, cada uno con subject_login, subject_name y subject_kind; subject es entonces un filtro libre, y sin ser dueño de nada no lista ninguno. Un admin recibe la misma lista con él o sin él. Cualquier otro valor es 400 query.bad_managed. cluster_name es el nombre del cluster, legible por cualquier sesión (/api/clusters es solo de admin). group es lo que recibe el cluster: el grupo de Kubernetes al que se liga el permiso o, en un rol nativo, los grupos del rol separados por ,; groups es lo mismo como lista, un elemento por grupo. subject_disabled es true si la cuenta del sujeto está deshabilitada: el permiso se conserva pero no se aplica. Cada permiso lleva source: {"kind": "direct"}, {"kind": "group", "group_id", "group_name", "member_id", "group_grant_id"} si deriva de un grupo, o {"kind": "set", "set_grant_id", "selector"} si lo puso en el cluster un permiso sobre un conjunto (el permiso de un grupo sobre un conjunto da a los permisos de sus miembros kind group, con set_grant_id y selector además); sin rol de admin, tus propios permisos también lo llevan, nombre del grupo incluido. group=<id> lista solo los permisos derivados de ese grupo (400 query.bad_group si el id no es válido). Para un admin, un permiso activo que un problema de su cluster deja sin efecto lleva effect: {"kind": "ineffective", "code", "name", "name_kind", "reason", "victims"} en el permiso que lo causa (code es el del problema, victims cuántas cuentas deja sin acceso al cluster), o {"kind": "blocked", "name", "by"} en otro permiso de una de esas cuentas en él (by es el primer culpable del problema, como en el problems del cluster). Sin rol de admin, effect no aparece nunca. Toda vista de un permiso lleva request_id: la solicitud de acceso cuya aprobación lo creó o lo amplió por última vez, null en uno concedido a mano. Con él vienen request_decided_by (el login de quien aprobó la solicitud; vacío si la concedió la regla de un entorno), request_decided_as (admin, owner o policy) y request_extended (true si esa solicitud amplió el permiso en vez de crearlo); sin request_id los tres vienen vacíos o false, y los lee quien puede leer el permiso.
POST /api/grants Admin, dueño Concede: {"subject_id", "cluster_id", "role", "scope", "expires_at", "never_expires", "note", "ticket"}. Sin expires_at, o con "expires_at": null, el permiso recibe la caducidad por defecto: 30 días o, para una persona, la del entorno del cluster; "never_expires": true lo deja sin caducidad, y mandarlo junto con expires_at es 400 grant.expiry_conflict. En un cluster cuyo entorno tiene duración máxima, el permiso de una persona que termina después de ahora más el máximo, o nunca, es 400 grant.environment_max, con params.environment y params.max. cluster-admin no admite ni el valor por defecto ni never_expires: necesita un expires_at de 8 h como máximo (400 grant.expiry_required). ticket es opcional, de 200 caracteres como mucho y sin caracteres de control ni de formato (400 grant.ticket_too_long, 400 grant.ticket_invalid): una URL https:// con un nombre de host ASCII, una clave como INC-123 (escrita en cualquier caso, se guarda en mayúsculas) o un número como #512. Un cluster cuyo entorno pide motivo o ticket rechaza el permiso de una persona o de un grupo que no lo lleve, con 400 grant.reason_required o 400 grant.ticket_required y params.environment (y params.index en un lote); el permiso de un bot, ampliar y revocar nunca los piden. Toda vista de un permiso lleva ticket junto a note, "" si no hay. El created_by_login de la respuesta es el login de quien llama. tier se acepta como alias obsoleto de role, y cada permiso de una respuesta lleva los dos con el mismo valor.
POST /api/grants/preview Admin, dueño Lo que se encontraría al crear un permiso, sin escribir nada: {"subject_id" o "group_id", "cluster_id", "role", "scope", "expires_at"?, "never_expires"?} (exactamente uno de subject_id y group_id; la caducidad se resuelve como en POST /api/grants, con la del entorno para una persona o un grupo) → {"username", "missing_fact", "invalid_name", "requires_expiry", "max_ttl_seconds", "reasons", "violations"}. username es el nombre con el que la persona entraría en el cluster (null en un permiso de grupo, o cuando no se puede rendir: entonces missing_fact nombra la variable que le falta a la cuenta, o invalid_name es true). requires_expiry y max_ttl_seconds dicen si la regla de las 8 horas pide caducidad y de cuánto como máximo; reasons dice por qué, cada una con privilege, la primera razón que da el cluster (como en rbac-subjects): {"kind": "group_privileged", "group", "privilege"} (un grupo de un rol nativo), {"kind": "user_privileged", "username", "privilege"} (el nombre que el cluster rinde para la persona), {"kind": "role_privileged", "group", "privilege"} (un rol que escribe kubelatch, en su ámbito: group es el grupo de kubelatch para él, y privilege.namespace es el ámbito) o {"kind": "group_permission_refused", "group", "privilege"}. violations son los rechazos con código que daría la petición real, cada uno con code, params y field. Lee el cluster como la petición real (502 cluster.unreachable si no puede), y guarda lo leído cinco segundos.
POST /api/grants/batch Admin, dueño Concede de 1 a 200 permisos a la vez: {"grants": [{"subject_id", "cluster_id", "role", "scope", "expires_at", "never_expires", "note", "ticket"}], "reason": ""} → 201 con {"batch_id", "grants": [...]}, cada caducidad como en POST /api/grants. Las entradas pueden nombrar varios clusters. Todo o nada; una entrada fallida responde con su propio código (grant.exists, account.disabled, cluster.not_ready, un cluster inalcanzable…) y su posición en params.index. Un namespace sin PSA es 400 grant.psa_required. 400 grant.batch_size fuera de los límites. El cuerpo puede llegar a 512 KiB, sitio para 200 entradas con una nota de 500 caracteres cada una (las demás rutas admiten 64 KiB); por encima, 413 request.body_too_large con el límite en params.limit, en KiB. La petición lee cada cluster una sola vez, sean cuantas sean las entradas: una lista de sus namespaces y una lectura de su RBAC. reason es opcional, de 500 caracteres como máximo (400 batch.reason_too_long); cada fila grant.create lleva batch (el batch_id), batch_size (las entradas) y reason si se dio, como en las operaciones en lote.
DELETE /api/grants/{id} Admin, dueño Revoca un permiso; el created_by_login de la respuesta es el login de quien lo creó, vacío para un dueño. 409 grant.derived para un permiso derivado de un grupo, con el id del grupo en params.group_id: quita al miembro o revoca el permiso del grupo. 409 grant.set_derived para uno que puso ahí un permiso sobre un conjunto, con params.selector y params.set_grant_id: revoca en su lugar el permiso sobre el conjunto, o saca el cluster del conjunto. Un permiso derivado ya revocado responde 404 grant.not_found.
POST /api/grants/revoke Admin, dueño Revoca de 1 a 500 permisos a la vez: {"ids": [...], "reason": ""} → 200 con {"batch_id", "grants": [...], "unchanged": [...]}, como en operaciones en lote. Los ids son los de permisos directos: 409 grant.derived, con el id del grupo en params.group_id, para uno derivado de un grupo (quita al miembro o revoca el permiso del grupo), 409 grant.set_derived, con params.selector y params.set_grant_id, para uno que puso ahí un permiso sobre un conjunto, 404 grant.not_found para uno que no existe. Un permiso ya revocado no es un fallo: su id vuelve en unchanged.
PATCH /api/grants/{id} Admin, dueño Amplía un permiso directo con uno de {"extend_by_seconds": n} (suma n segundos, más de 0, a su caducidad), {"expires_at": "<RFC 3339>"} (una fecha nueva, posterior o anterior) y {"never_expires": true} (sin fecha de fin); ninguno o más de uno es 400 batch.extend_choice. → el permiso, como lo enseña la lista. La caducidad nueva pasa las comprobaciones de un permiso nuevo (grant.expiry_past, grant.expiry_required, grant.expiry_too_far, grant.environment_max, el namespace y su etiqueta PSA…), y la regla de las 8 horas cuenta la vida del permiso desde que se concedió: más de 8 horas de vida en un nombre privilegiado es 409 grant.privileged_lifetime, con params.created_at; revócalo y vuelve a concederlo. 409 batch.no_expiry para un tiempo sobre un permiso sin fecha de fin; 409 grant.expired, 409 grant.revoked, 409 grant.derived (con params.group_id) o 409 grant.set_derived (con params.selector y params.set_grant_id) para uno que no es un permiso directo activo; 409 account.disabled para el de una cuenta deshabilitada, como al concederlo; 404 grant.not_found. Escribe una fila grant.expiry con from y to, y reconcilia el cluster.
POST /api/grants/extend Admin, dueño Amplía de 1 a 50 permisos con una sola decisión: {"ids": [...], "reason": ""} más uno de extend_by_seconds, expires_at y never_expires, como en el PATCH individual → 200 con {"batch_id", "grants": [...], "changes": [{"id", "from", "to"}]}, como en operaciones en lote. extend_by_seconds suma a la caducidad de cada permiso, así que nunca acorta ninguno; expires_at pone una misma fecha a todos y puede acortar alguno; changes reúne la caducidad de cada uno antes y después (null si no tiene). Un permiso que falla responde el código del PATCH individual con params.index y params.id.
GET /api/tiers Sesión Catálogo de niveles, namespaces protegidos, si se exige PSA, la credencial más larga que puede recibir una persona (user) y un bot (credential_max_ttl_seconds), access_contact, a quién pedir acceso: el valor de KUBELATCH_ACCESS_CONTACT o, si no está puesta, mailto: y la dirección del administrador habilitado más antiguo que tenga una, "" si ninguno la tiene; access_admin, el login de ese administrador cuando access_contact es "" (si no, ""), y ticket_url, la plantilla que enlaza una clave o un número de ticket con el tracker (KUBELATCH_TICKET_URL, con {key} una vez), vacía si no está puesta.

Permisos sobre un conjunto

Todas las rutas piden el rol de admin. Un permiso sobre un conjunto da a una cuenta o a un grupo un rol en todos los clusters de un entorno o con una etiqueta, y sigue al conjunto. En cada cluster donde lo tiene, es un permiso de /api/grants (source.kind set). Conceder y ampliar piden Pro (403 edition.feature, sin escribir nada); las vistas previas, volver a comprobar y revocar funcionan en cualquier edición.

Un conjunto es {"kind": "environment", "key": "", "value": "staging"} o {"kind": "label", "key": "equipo", "value": "pagos"}. En el filtro de una lista, un entorno es environment=staging y una etiqueta label=equipo:pagos. Un permiso sobre un conjunto es {"id", "subject_id", "login", "subject_kind", "subject_disabled", "group_id", "group_name", "selector", "role", "scope", "expires_at", "note", "ticket", "created_by", "created_by_login", "created_at", "revoked_at", "revoked_by", "active", "clusters"} (subject_id o group_id, el otro null), y clusters tiene una entrada por cada cluster del conjunto hoy: {"cluster_id", "environment", "row", "refusal"}, donde row es {"id", "expires_at"} cuando el cluster tiene el permiso y refusal es {"code", "params", "kind", "decided_at"} cuando no lo tiene (kind es transient o rule; Cuándo decide kubelatch otra vez). Mientras un cluster espera a que le vuelvan a preguntar tras una ampliación, su row.expires_at puede ser anterior al del permiso. Lo que listan lost, gained y pending son cambios: {"set_grant_id", "selector", "cluster_id", "subject_id", "login", "subject_kind", "group_id", "group_name", "role", "scope", "expires_at"}, más code y params cuando le quitó el permiso una regla, y no el conjunto.

Método Ruta Qué hace
GET /api/cluster-set-grants {"set_grants": [...]}, el más reciente primero. Filtros: subject (los propios de una cuenta y los de los grupos a los que pertenece), group, role (el id de un rol; uno que ningún permiso usa no coincide con nada), environment o label (no los dos) e include_revoked=true. 400 query.subject_not_uuid, 400 query.bad_group, 400 set_grant.selector_invalid.
POST /api/cluster-set-grants Concede: {"subject_ids": [...], "selector", "role", "scopes": [...], "expires_at", "never_expires", "note", "ticket"}, con group_id en lugar de subject_ids para un grupo → 201 con {"batch_id", "set_grants": [...]}. Un permiso por cuenta (o el grupo) y ámbito, de 1 a 50, todo o nada (400 grant.batch_size); una entrada fallida responde con su propio código y su posición en params.index. Exactamente uno de subject_ids y group_id (400 set_grant.target). La caducidad se resuelve como en POST /api/grants, con la del entorno para una persona o un grupo, y el máximo de un entorno vale para un permiso sobre ese entorno (400 grant.environment_max). Los ámbitos son * o namespaces. 400 set_grant.cluster_admin, 400 set_grant.selector_invalid, 400 environment.not_found, 409 set_grant.exists, 409 account.disabled, 409 role.stale_revision si un rol personalizado se editó mientras tanto. Se pregunta a cada cluster del conjunto, y el que no pasa es un refusal de la respuesta: la petición no falla por él. Registra set.grant.create por cada permiso, con el grant.create o group.grant.create de cada cluster al que llega, todos con un mismo batch.
POST /api/cluster-set-grants/preview Lo que haría POST con el mismo cuerpo, sin escribir nada y sin pedir Pro: {"clusters": [...], "entries": [{"index", "subject_id", "group_id", "scope", "reaches": [...], "refused": [{"cluster_id", "code", "params", "kind"}]}]}. Rechaza lo que rechaza POST antes de preguntar a ningún cluster, con los mismos códigos.
GET /api/cluster-set-grants/{id} Un permiso sobre un conjunto, revocado o no. 404 set_grant.not_found.
PATCH /api/cluster-set-grants/{id} Lo amplía con uno de {"extend_by_seconds": n}, {"expires_at": "<RFC 3339>"} y {"never_expires": true}, como PATCH /api/grants/{id} → {"set_grant", "lost", "pending"}. Se pregunta a cada cluster del conjunto con la caducidad nueva: lost lista los permisos que dejan de llegar a un cluster porque sus reglas no admiten la fecha, y pending los clusters a los que no se pudo preguntar (o que entraron en el conjunto entretanto), cuyo permiso conserva su fecha hasta que kubelatch lo reintente. 400 batch.extend_choice, 400 grant.environment_max, 409 batch.no_expiry, 409 grant.expired, 409 grant.revoked, 409 account.disabled, 409 role.stale_revision, 404 set_grant.not_found. Registra set.grant.expiry.
POST /api/cluster-set-grants/{id}/preview Lo que haría PATCH con el mismo cuerpo, sin escribir nada y sin pedir Pro: {"to", "lost", "pending"}.
POST /api/cluster-set-grants/{id}/recheck Vuelve a comprobar los clusters del conjunto que no tienen el permiso, o lo tienen más corto → {"set_grant", "gained"}, con gained listando lo que trajo. Registra set.grant.recheck aunque no traiga nada. 409 grant.revoked, 409 grant.expired, 409 account.disabled (el permiso de una cuenta deshabilitada solo se revoca), 404 set_grant.not_found.
POST /api/cluster-set-grants/{id}/recheck/preview Lo que traería volver a comprobar, sin escribir nada: {"gained"}; rechaza lo mismo que volver a comprobar, con los mismos códigos.
DELETE /api/cluster-set-grants/{id} Revoca el permiso y lo que puso en cada cluster → 200 con él. 404 set_grant.not_found para uno que no existe, está mal formado o ya se revocó. Registra set.grant.revoke, y un grant.revoke por cluster en el permiso de una cuenta; los permisos de los miembros de un grupo los revoca la sincronización del grupo. No lleva cuerpo y no registra motivo.
POST /api/cluster-set-grants/{id}/revoke La misma revocación con el motivo de una baja: {"reason": ""}, opcional (sin cuerpo, o con el motivo vacío, es el DELETE), de 500 caracteres como máximo (400 batch.reason_too_long). Cada evento que registra (set.grant.revoke, y el grant.revoke o group.grant.revoke de cada cluster) lleva un mismo batch y el reason, como en operaciones en lote. Mismas respuestas que el DELETE.

Los errores están en Errores.

Solicitudes de acceso

Una persona pide un rol en un cluster o un namespace durante un tiempo, con un motivo y quizá un ticket, y un administrador lo aprueba o lo deniega; el entorno de un cluster puede conceder algunos roles sin aprobación (auto_approve_roles, en Entornos). El permiso que da una aprobación dura desde la aprobación, y su grant.create lleva request_id (Auditoría). Presentar una solicitud es Pro (403 edition.feature en Free, y con una clave caducada); decidir lo que espera, no. Decide cualquier administrador habilitado que no sea quien pide. Las mismas rutas sirven las solicitudes de alta de quien no tiene cuenta (Incorporación), a las que solo llega un administrador.

Una solicitud de un rol es {"id", "kind": "access", "status", "subject_id", "login", "name", "cluster_id", "cluster_name", "role", "scope", "duration_seconds", "reason", "ticket", "origin", "origin_detail", "needs_admin", "extends_grant_id", "created_at", "expires_at", "decided_by", "decided_as", "decided_at", "decision_note", "result_grant_id", "granted_until"}:

  • status es pending, approved, denied, expired o cancelled. Una solicitud espera 24 horas (expires_at), y pasadas se lee expired al momento.
  • scope es un namespace o * para todo el cluster; duration_seconds es lo que pide, contado desde la aprobación.
  • origin es desde dónde se presentó: web, cli, forbidden (el enlace del 403 de un cluster, con la petición rechazada en origin_detail, solo ASCII imprimible) o notice.
  • needs_admin dice que solo un administrador puede decidirla; extends_grant_id nombra el permiso directo del mismo rol y ámbito que una aprobación amplía en vez de conceder de nuevo (null si no).
  • decided_by es {"id", "login", "name"}, null hasta que decide una persona; decided_as es admin, o policy para la regla del entorno, con decided_by null. decision_note es lo que escribió quien decidió para quien pidió, result_grant_id el permiso que creó o amplió una aprobación y granted_until cuándo termina.
Método Ruta Acceso Qué hace
POST /api/access-requests Sesión, token de la CLI Pide un rol: {"cluster_id", "role", "scope", "duration_seconds", "reason", "ticket", "origin", "origin_detail"}. role es un nivel, un rol personalizado que admite el cluster o un rol nativo del cluster, y scope encaja con el rol como en POST /api/grants. La duración va de 15 minutos a la menor entre el máximo del entorno, la del rol (8 horas en cluster-admin) y 30 días (400 access_request.duration, con params.max). reason es obligatorio, de 1 a 500 caracteres (400 access_request.reason_required, 400 grant.note_too_long), sin caracteres de control ni de formato salvo saltos de línea y tabuladores (400 grant.note_invalid); ticket es opcional, como en POST /api/grants, salvo que el entorno lo pida (400 grant.ticket_required). origin es web si falta (400 request.bad_json con otro valor). → 201 con {"request", "grant": null, "existing": false}; cuando el entorno concede el rol sin aprobación, request.status es approved y grant es el permiso. La misma solicitud esperando ya responde 200 con {"request", "grant": null, "existing": true}. 400 access_request.not_requestable (un namespace protegido, un rol que el cluster no admite, un bot), los errores de forma del rol como en POST /api/grants (400 grant.cluster_only…), 404 cluster.not_found (desconocido o sin enrolar), 409 access_request.pending_limit (20 esperando), 429 access_request.too_many (30 presentadas en la última hora), 409 access_request.no_decider (nadie más puede decidirla), 409 grant.exists (ya lo tienes sin fin) y los errores propios del permiso cuando lo concede la regla del entorno. Registra access_request.create.
GET /api/access-requests Sesión, token de la CLI (view=mine) Un array de solicitudes de un rol, como mucho 200: ?view=mine, las tuyas, la más reciente primero; ?view=waiting, las que puedes decidir ahora, la más antigua primero, nunca las tuyas; ?view=decided, las que decidiste o, para un administrador, todas las decisiones de los últimos 30 días, la más reciente primero. kind=access es el valor por defecto; kind=join da las listas de alta (Incorporación). 400 query.bad_view, 400 query.bad_kind; un token de la CLI recibe 403 cli.scope en todo salvo view=mine.
GET /api/access-requests/{id} Sesión, token de la CLI (las tuyas) Una solicitud, para quien la pidió, para un administrador y para un dueño de su sitio, con can_decide (false para un dueño si solo la decide un administrador), deciders ([{"login", "name"}], a quién se le pide decidirla, como mucho 50 por login), deciders_more (cuántos más hay), decider_scope (el sitio del que son dueños los deciders: {"kind": "namespace", "cluster_id", "cluster_name", "namespace"}, {"kind": "cluster", "cluster_id", "cluster_name"} o {"kind": "environment", "environment_id"}; null cuando deciden los administradores), held (los permisos activos de quien pide en ese cluster, para un dueño solo los de lo que gestiona: [{"id", "role", "scope", "expires_at", "source"}], con source direct, group o set), max_seconds (lo más que puede dar ahora una aprobación; 0 si no puedes decidirla) y extends_until (en una prórroga de un permiso activo, el final que pide; si no, null). 404 access_request.not_found para cualquier otra persona y para el id de una solicitud de alta.
POST /api/access-requests/{id}/approve Sesión (quien decide) Aprueba una solicitud de un rol: {"duration_seconds", "note"}, los dos opcionales. La duración solo puede acortar la pedida (400 access_request.longer; por debajo de 15 minutos, 400 access_request.duration); la nota, de hasta 500 caracteres (400 grant.note_too_long) y sin caracteres de control ni de formato salvo saltos de línea y tabuladores (400 grant.note_invalid), es para quien pidió. → {"request", "grant"}: el permiso, con quien pidió como sujeto, el motivo como nota y el ticket de la solicitud, que termina duration_seconds a partir de ahora; una prórroga amplía el permiso en su sitio, o lo concede de nuevo si ya terminó. 403 access_request.self, 404 access_request.not_found (quien no puede decidirla, antes de leer el cuerpo), 403 access_request.admin_only (un dueño, en una solicitud que solo decide un administrador o en un permiso que ahora el cluster hace privilegiado; la solicitud queda entonces marcada), 409 access_request.closed (decidida, cancelada o pasadas sus 24 horas), 403 access_request.not_decider y los errores propios del permiso (400 grant.psa_required, 422 grant.privileged_ttl…), que dejan la solicitud esperando. Registra access_request.approve. Una solicitud de alta lleva el cuerpo de Incorporación.
POST /api/access-requests/{id}/deny Sesión (quien decide) Deniega una solicitud de un rol: {"note"}, de hasta 500 caracteres, para quien pidió → la solicitud. Los mismos rechazos que al aprobar. Registra access_request.deny. Una solicitud de alta, como en Incorporación.
POST /api/access-requests/approve Sesión (quien decide) Aprueba varias solicitudes de un rol a la vez, todas o ninguna: {"ids", "note"}, de 1 a 50 ids distintos (uno repetido cuenta una vez; 400 access_request.batch_size, con params.max) y una nota para todas, con las reglas de la de una aprobación suelta. Cada solicitud se aprueba por la duración que pide. → {"batch_id", "requests"}: las solicitudes ya decididas, en el orden pedido, cada una como la devuelve en request una aprobación suelta. Cada id pasa primero las comprobaciones de una aprobación suelta y después una sola transacción las aprueba todas: la primera que falla responde el error que daría sola, con params.index, su posición en ids, y no se decide ninguna. Una solicitud de alta es 400 access_request.batch_kind para un administrador. Quien no decide nada recibe 403 session.admin_only antes de que se lea el cuerpo. Registra un access_request.approve por solicitud y el grant.create o el grant.expiry de cada permiso, todos con batch y batch_size.
POST /api/access-requests/deny Sesión (quien decide) Deniega varias solicitudes a la vez, todas o ninguna: {"ids", "note"}, de 1 a 100 ids distintos (400 access_request.batch_size, con params.max), solicitudes de un rol y, para un administrador, también de alta. La nota sigue la regla de cada tipo que hay en el lote (400 grant.note_too_long, 400 grant.note_invalid, 400 join.bad_note): la lee quien pidió un rol, nunca quien pidió el alta. → {"batch_id", "requests"}, en el orden pedido, cada solicitud como la devuelve su denegación suelta, distinguidas por kind. La primera que falla responde el error que daría sola, con params.index, y no se decide ninguna; una solicitud de alta que nombra quien no es administrador es 404 access_request.not_found, igual que un id desconocido. Quien no decide nada recibe 403 session.admin_only antes de que se lea el cuerpo. Registra un access_request.deny por solicitud, con batch y batch_size.
POST /api/access-requests/{id}/cancel Sesión, token de la CLI Retira una solicitud tuya mientras espera; sin cuerpo, o {} → la solicitud, cancelled. 404 access_request.not_found para la de otra persona, 409 access_request.closed. Registra access_request.cancel.
GET /api/request-options Sesión, token de la CLI Lo que puede nombrar una solicitud: {"requests_enabled", "contact", "contact_admin", "clusters": [{"id", "name", "environment", "production"}]}, los clusters enrolados, con environment el id del entorno del cluster o "". contact es a quién pedir, como access_contact en GET /api/tiers, y contact_admin el administrador al que nombrar cuando es "". Con ?cluster=<id> añade cluster: los mismos cuatro campos, rules ({"max_seconds", "default_seconds", "require_reason", "require_ticket"}, del entorno, con max_seconds null sin máximo), roles ([{"id", "name", "description", "kind", "scopes", "auto_approve", "max_seconds", "proposed_seconds"}], con kind tier, custom o native, scopes con namespace y/o cluster, y proposed_seconds la duración que propone el formulario), deciders, deciders_more, decider_scope y held, como en el detalle de una solicitud (cada entrada de held añade extend_max_seconds: lo más que una solicitud puede ampliar ahora ese permiso, en segundos, 0 si no cabe nada, y null si una solicitud no puede ampliarlo, porque llega por un grupo o un conjunto o no tiene final), para una solicitud en todo el cluster. Con &namespace=<name> los tres campos de quién decide responden por una solicitud en ese namespace: sus propios dueños si los tiene, y si no los del cluster. Una solicitud que solo decide un administrador se pide a los administradores digan lo que digan estos campos. En Free, requests_enabled es false, clusters está vacío y no hay cluster. 400 query.bad_cluster, 400 query.bad_namespace, 404 cluster.not_found.

Los errores están en Errores.

Dueños

Un administrador nombra a los dueños de un entorno, un cluster, un namespace de un cluster o un grupo. Nombrar uno es de Pro (403 edition.feature en Free y con la clave caducada); listarlos y quitarlos funciona en cualquier edición.

Un ámbito es {"kind": "environment", "environment_id"}, {"kind": "cluster", "cluster_id", "cluster_name"}, {"kind": "namespace", "cluster_id", "cluster_name", "namespace"} o {"kind": "group", "group_id", "group_name"}. Un dueño es {"id", "scope", "owner", "inherited", "created_by_login", "created_at"}, donde owner es una persona, {"subject_id", "login", "display_name", "disabled"}, o un grupo, {"group_id", "group_name", "members"}, e inherited marca a un dueño del entorno listado en uno de sus clusters.

Método Ruta Acceso Qué hace
GET /api/owners Admin Los dueños de ?environment=<id>, ?cluster=<id> o ?group=<id>, exactamente uno (400 query.bad_owner_scope) → {"owners": [...]}, el más antiguo primero. Un cluster lista los suyos, los de sus namespaces y, con inherited, los de su entorno. Un id fuera de formato es 400 query.bad_environment, 400 query.bad_cluster o 400 query.bad_group; uno desconocido, 404 environment.not_found, 404 cluster.not_found o 404 group.not_found.
POST /api/owners Admin Nombra a un dueño: {"scope": {"kind", "environment_id" \| "cluster_id" [, "namespace"] \| "group_id"}, "owner": {"subject_id"} \| {"group_id"}} → 201 con el dueño. 400 owner.invalid_scope, 400 owner.not_person, 409 owner.exists, y 404 subject.not_found, 404 group.not_found, 404 environment.not_found o 404 cluster.not_found para lo que no existe. Registra owner.add.
DELETE /api/owners/{id} Admin Quita a un dueño → 204; 404 owner.not_found. Registra owner.remove.
GET /api/managed Sesión Lo que gestionas como dueño: {"sites", "groups", "people", "all_groups", "group_grants"}. sites son los clusters en los que eres dueño de algo, por id: {"cluster_id", "cluster_name", "environment", "production", "whole", "namespaces", "max_ttl_seconds", "default_ttl_seconds", "require_reason", "require_ticket"}, con whole para el dueño del cluster o de su entorno (y entonces namespaces es []), si no, los namespaces de los que eres dueño; los cuatro últimos son las reglas del entorno del cluster, null y false en un cluster sin entorno. groups son los grupos de los que eres dueño, {"id", "name"}. people son las personas habilitadas, {"id", "login", "display_name"}, por login. Al dueño de un sitio, all_groups le lista todos los grupos por nombre y group_grants los permisos de grupo en vigor en sus sitios, {"id", "group_id", "group_name", "cluster_id", "cluster_name", "role", "scope", "expires_at", "note", "ticket", "created_at"}. Un administrador, y quien no es dueño de nada, recibe cinco listas vacías.

Los errores están en Errores.

Grupos

Un grupo es un conjunto con nombre de personas y bots con permisos propios; cada miembro recibe esos permisos como filas derivadas de /api/grants (source.kind group). Un administrador usa todas las rutas. En Pro, los dueños de un grupo lo leen (GET /api/groups lista solo los grupos de los que son dueños, sin member; el GET /api/groups/{id} de otro grupo es 404 group.not_found) y añaden, amplían, pasan y quitan a sus miembros; los dueños de un sitio añaden, amplían y revocan permisos de grupo en él. Dentro de sus límites: fuera de lo que gestionan 403 owner.out_of_scope, darse acceso a sí mismos (añadirse o pasarse, ampliar su propia pertenencia, un permiso para un grupo del que son miembros) 403 owner.self, un bot añadido, pasado o ampliado y el permiso de grupo de un rol nativo 403 owner.admin_only, y los miembros de un grupo que es dueño de algo 409 owner.group_owns con params.scope. Cualquier otra persona recibe 403 session.admin_only antes de que se lea el cuerpo. Crear, renombrar y eliminar un grupo siguen siendo de un administrador. Los eventos de los dueños llevan "as": "owner" y owner_scope.

Método Ruta Acceso Qué hace
GET /api/groups Admin, dueño {"groups": [...]}, por nombre. Cada grupo: id, name, description, members y grants (recuentos de los no caducados), clusters (donde tiene permisos activos), created_at y updated_at. ?member=<id del sujeto> lista solo los grupos a los que pertenece ese sujeto (400 query.subject_not_uuid si el id no es un uuid), y cada uno lleva membership: {"id", "expires_at"}, la fila de ese sujeto en el grupo (expires_at es null si no caduca). Su id es el {mid} de las rutas de miembros de más abajo. Sin member, membership no aparece. ?members=1 (o true) añade members_list a cada grupo: sus pertenencias vivas, caducadas incluidas, con la forma de GET /api/groups/{id} y de más antigua a más reciente ([] si no tiene ninguna); se combina con member, y sin él members_list no aparece. 0, false y un valor vacío lo apagan; cualquier otro valor es un 400 query.bad_filter. GET /api/groups?grants=1 (o true) añade grants_list a cada grupo: sus permisos activos, con la forma de GET /api/groups/{id} y por clúster, ámbito e id ([] si no tiene ninguno); se combina con members y member, y sin él grants_list no aparece. Admite los mismos valores que members.
POST /api/groups Admin Crea un grupo: {"id", "name", "description"} → 201 con el grupo. El id (de 1 a 63 letras minúsculas, dígitos o guiones) no se reutiliza.
GET /api/groups/{id} Admin, dueño El grupo más members_list (id, subject_id, login, kind, display_name, disabled, expires_at, note, ticket, created_by_login, created_at, source y signup_domain; source es manual, o domain para un miembro que añadió una regla de dominio, cuyo signup_domain nombra la regla y cuyo created_by_login es null) y grants_list (id, cluster_id, role, scope, group, groups, expires_at, note, ticket, created_by_login, created_at, y set, {"set_grant_id", "selector"}, en un permiso que puso en el grupo un permiso sobre un conjunto; group es lo que recibe el cluster: el grupo de Kubernetes al que se liga el permiso o, en un rol nativo, los grupos del rol separados por ,, y groups es lo mismo como lista), y owns, los ámbitos de los que el grupo es dueño, cada uno como el scope de un dueño ([] si ninguno). 404 group.not_found.
PATCH /api/groups/{id} Admin Cambia name y description; un campo omitido se queda como está.
DELETE /api/groups/{id} Admin Borra el grupo y revoca todos los permisos derivados de él, y sus permisos sobre un conjunto → 204. Sus dueños, y los ámbitos de los que el grupo era dueño, se van con él (owners_removed en group.delete). Su id no se reutiliza. 409 signup_domain.group_in_use mientras una regla de dominio apunte a él.
POST /api/groups/{id}/members Admin, dueño Añade miembros: {"members": [{"subject_id", "expires_at", "note", "ticket"}]}, de 1 a 100 → 201 con {"members": [...]}. Todo o nada. Cada miembro recibe al instante los permisos del grupo. Antes de añadir a nadie, cada permiso activo del grupo se vuelve a comprobar como se comprobaría concederlo directamente; si uno ya no pasa, la llamada responde con el código de esa comprobación, params.group_grant_id y params.index 0. 409 group.member_exists, 409 group.subject_disabled.
PATCH /api/groups/{id}/members/{mid} Admin, dueño Cambia la caducidad de un miembro: {"expires_at": "<RFC 3339>"}. Un expires_at ausente o null significa nunca. → el miembro. Cada permiso derivado del miembro toma la nueva caducidad, acotada por la de su permiso del grupo. Devolver la vida a una pertenencia caducada comprueba antes los permisos del grupo, como al añadir un miembro. 400 group.expiry_past; 409 account.disabled para la pertenencia de una cuenta deshabilitada, como se rechaza añadirla.
DELETE /api/groups/{id}/members/{mid} Admin, dueño Retira a un miembro y revoca los permisos derivados para él → 204.
POST /api/groups/{id}/members/remove Admin, dueño Retira de 1 a 500 miembros a la vez: {"ids": [...], "reason": ""} → 200 con {"batch_id", "members": [...], "unchanged": [...]}, como en operaciones en lote. Los ids son el id de cada entrada de members_list, no el de la cuenta. Cada miembro sale como en el DELETE individual, con los permisos derivados para él. 404 group.member_not_found para un id que no es miembro del grupo. Un miembro ya retirado vuelve en unchanged.
POST /api/groups/{id}/members/extend Admin, dueño Amplía de 1 a 50 miembros con una sola decisión: {"ids": [...], "reason": ""} más uno de extend_by_seconds, expires_at y never_expires, como en POST /api/grants/extend → 200 con {"batch_id", "members": [...], "changes": [{"id", "from", "to"}]}, como en operaciones en lote. Los ids son el id de cada entrada de members_list. Cada miembro cambia como en el PATCH individual, con los permisos derivados para él; devolver la vida a una pertenencia caducada comprueba antes los permisos del grupo. Un miembro que falla responde el código del PATCH individual con params.index y params.id: 404 group.member_not_found para un id que no es miembro del grupo, 409 batch.no_expiry para sumar tiempo a un miembro sin fecha de fin, 400 group.expiry_past, 409 account.disabled.
POST /api/groups/{id}/members/move Admin, dueño Pasa de 1 a 500 miembros a otro grupo en un solo paso: {"ids": [...], "to_group_id", "reason": ""} más exactamente uno de expires_at (la caducidad de todas las pertenencias nuevas, null para ninguna) y "keep_expiry": true (cada una conserva la suya) → 200 con {"batch_id", "moved": [...], "unchanged": [...]}, como en operaciones en lote. Los ids son el id de cada entrada de members_list. Cada miembro sale de este grupo como en el DELETE individual y entra en to_group_id como en POST /api/groups/{id}/members, con las comprobaciones de ese grupo: antes se vuelven a comprobar sus permisos activos (un fallo responde el código de esa comprobación con params.group_grant_id y params.index 0), y una cuenta deshabilitada responde 409 group.subject_disabled. moved reúne las pertenencias nuevas, como las enseña el members_list del destino. Un miembro que ya está en el destino solo sale de este grupo, conserva esa pertenencia tal como está y vuelve en unchanged. 400 group.move_choice sin exactamente uno de los dos, 400 group.move_same si to_group_id es este grupo, 404 group.not_found para cualquiera de los dos grupos, 404 group.member_not_found para un id que no es miembro de este grupo, 400 group.expiry_past para una fecha pasada o una caducidad conservada que ya pasó.
POST /api/groups/{id}/grants Admin, dueño Da permisos al grupo: {"grants": [{"cluster_id", "role", "scope", "expires_at", "never_expires", "note", "ticket"}]}, de 1 a 50 → 201 con {"grants": [...]}, cada caducidad como en POST /api/grants: sin expires_at, la caducidad por defecto del entorno del cluster o 30 días; ninguna con "never_expires": true. En un cluster cuyo entorno tiene duración máxima, una caducidad que la supera, o ninguna, es 400 grant.environment_max, sean quienes sean los miembros del grupo. Todo o nada. Cada miembro activo los recibe. cluster-admin es personal: 400 group.cluster_admin; un rol nativo cuyo grupo recibe acceso privilegiado del cluster, o un rol que escribe kubelatch en un namespace privilegiado, 422 group.privileged.
PATCH /api/groups/{id}/grants/{gid} Admin, dueño Cambia la caducidad de un permiso del grupo: {"expires_at": "<RFC 3339>"}, ausente o null para nunca. → el permiso. Cada fila derivada de él toma la nueva caducidad, acotada por la de su pertenencia; un permiso caducado vuelve. El destino se valida otra vez como un permiso nuevo (códigos grant.*). 400 group.expiry_past, 404 group.grant_not_found, 409 grant.set_derived para uno que puso ahí un permiso sobre un conjunto.
DELETE /api/groups/{id}/grants/{gid} Admin, dueño Revoca un permiso del grupo y las filas derivadas de él → 204. 409 grant.set_derived para uno que puso ahí un permiso sobre un conjunto: revoca en su lugar ese permiso.
POST /api/groups/{id}/grants/revoke Admin, dueño Revoca de 1 a 500 permisos del grupo a la vez: {"ids": [...], "reason": ""} → 200 con {"batch_id", "grants": [...], "unchanged": [...]}, como en operaciones en lote. Los ids son el id de cada entrada de grants_list. Cada permiso se revoca como en el DELETE individual, con las filas derivadas de él. 404 group.grant_not_found para un id que no es un permiso del grupo. Un permiso ya revocado vuelve en unchanged.
POST /api/groups/{id}/grants/extend Admin, dueño Amplía de 1 a 50 permisos del grupo con una sola decisión, con el cuerpo de POST /api/groups/{id}/members/extend → 200 con {"batch_id", "grants": [...], "changes": [{"id", "from", "to"}]}. Los ids son el id de cada entrada de grants_list. Cada permiso cambia como en el PATCH individual, con las filas derivadas de él, y su destino se valida otra vez como un permiso nuevo (códigos grant.*). Un permiso que falla responde el código del PATCH individual con params.index y params.id: 404 group.grant_not_found para un id que no es un permiso del grupo, 409 batch.no_expiry, 400 group.expiry_past.

Una entrada fallida de un lote responde con su posición en params.index; un permiso que un conjunto puso en un grupo, nombrado en un lote del grupo, se rechaza con 409 grant.set_derived.

Operaciones en lote

Revocar, retirar y deshabilitar en lote comparten un mismo contrato. Cada ruta recibe {"ids": [...], "reason": "..."}:

  • ids: de 1 a 500, sin repetir. Son los ids de las filas que enseña la lista: el permiso, la pertenencia (el id de members_list, no el de la cuenta), el permiso del grupo o la cuenta.
  • reason: opcional, de 500 caracteres como máximo.

La respuesta es 200 con {"batch_id", "<elementos>", "unchanged": [...]}. batch_id es un uuid. <elementos> es grants, members o subjects, cada uno como lo devuelve su lista, en el orden pedido y sin los que no cambiaron. unchanged reúne los ids que ya estaban hechos (un permiso ya revocado, un miembro ya retirado, una cuenta ya deshabilitada): no son un fallo, y cada uno escribe igualmente su fila en el registro de control.

Un lote es todo o nada. Una petición con mala forma es 400 antes de leer nada: batch.size si no hay ids o hay más de 500, batch.duplicate si un id se repite (params.index es la posición de su segunda aparición y params.id el id) y batch.reason_too_long. Un elemento que falla responde con el estado y el código que respondería su operación individual, más params.index (su posición en la petición, desde 0) y params.id, y nada cambia. Las políticas son las de la operación individual: un lote rechaza lo que rechazaría una a una.

Cada elemento escribe una fila en el registro del plano de control con la acción que escribe su operación individual, más batch (el uuid), batch_size (los ids pedidos) y reason si se dio uno; la fila de un elemento sin cambios lleva unchanged: true. Tras el commit, cada cluster afectado se reconcilia una vez.

Ampliar en lote (POST /api/grants/extend, y members/extend y grants/extend de un grupo) sigue el mismo contrato con tres diferencias. Admite de 1 a 50 ids. Además de ids y reason recibe una sola decisión para todos, exactamente una de extend_by_seconds, expires_at y never_expires: si no, 400 batch.extend_choice, después de las comprobaciones de arriba. Su respuesta lleva changes, la caducidad de cada elemento antes y después, en lugar de unchanged.

Pasar miembros a otro grupo (members/move de un grupo) sigue el contrato de las retiradas, con moved, las pertenencias nuevas, como <elementos>, y en unchanged los miembros que ya estaban en el destino. Cada miembro escribe dos filas: group.member.remove en el grupo que deja, con to_group_id, y group.member.add en el que entra, con from_group_id; un miembro que ya estaba allí escribe su fila group.member.add con unchanged: true. El registro del plano de control muestra el lote en una sola línea, como un cambio de grupo.

Roles

Un rol es lo que concede un permiso: uno de los seis roles de sistema (los niveles), un rol personalizado o un rol nativo. Los roles personalizados se componen con el catálogo de capacidades y con reglas avanzadas ({"apiGroups", "resources", "verbs", "resourceNames"?}). Un rol nativo nombra en groups grupos que el cluster ya conoce, y nada más.

Método Ruta Acceso Qué hace
GET /api/roles Sesión {"roles": [...]}: los roles de sistema en su orden y después los personalizados por nombre. Cada rol: id, display_name, description, kind (custom o native; también custom en los roles de sistema), groups (los grupos del cluster de un rol nativo; siempre un array), system, duplicable, modules, rules, namespace_scope, cluster_scope, cluster_ids (null = todos los clusters), max_ttl_seconds (0 = sin límite), sensitive, requires_psa, revision, usage ({"grants", "clusters", "people", "bots", "permanent", "groups", "empty_groups", "sample"}: permisos activos de cuentas habilitadas, las personas y los bots que tienen el rol, los que tienen un permiso sin fin, los grupos vivos con un permiso activo que lo nombra (cada grupo cuenta una vez, sean cuantos sean sus clusters) y cuántos de ellos no tienen miembro vivo, y hasta tres titulares como {"login", "kind", "display_name"}, personas primero; sample es siempre un array), covers (los roles de sistema que hace redundantes en el mismo ámbito: un nivel lista los que incluye, un rol personalizado ninguno; siempre un array), created_at, updated_at y updated_by (el login de quien creó o editó por última vez un rol personalizado; null en un rol del sistema). Un rol borrado no aparece. Sin rol de admin, usage es siempre cero y cluster_ids es null, aquí y en GET /api/roles/{id}.
GET /api/role-activity Admin GET /api/role-activity: las peticiones de cada rol en los últimos 30 días, leídas de los grupos que envió el proxy: {"since", "floor", "roles": {"<id>": {"last_at", "daily"}}}, una entrada por rol (los de sistema incluidos). daily trae 30 cuentas, una por día UTC, de la más antigua a hoy; una petición cuenta una vez para un rol por muchos grupos suyos que llevara, y un grupo de namespace cuenta solo para una petición en ese namespace. Cuando la auditoría es más corta que 30 días (Free sirve 24 horas), floor es el primer momento leído, last_at cuenta desde él y daily es null; si no, floor es null.
GET /api/roles/{id} Sesión Un rol, más effective_rules y yaml (el ClusterRole que escribe kubelatch; vacío para admin y cluster-admin, y un rol nativo no tiene ninguno de los dos: [] y ""). 404 role.not_found, también para un rol borrado.
GET /api/roles/{id}/clusters Admin Lo que cada cluster listo sabe de los grupos de un rol nativo, como array: [{"cluster_id", "reachable", "groups": [{"name", "known", "privileged", "bindings"}]}], uno por cluster de los cluster_ids del rol (todos si es null), con bindings como en rbac-subjects. Un cluster que no se puede leer sale con reachable: false y sin grupos. Sale de la lectura de cinco minutos de cada cluster; ?refresh=1 los vuelve a leer, como en rbac-subjects. Un rol personalizado responde []; uno de sistema, 404 role.not_found.
POST /api/roles Admin Crea un rol personalizado: {"id", "display_name", "description", "modules", "rules", "cluster_scope", "cluster_ids"} → 201 con el rol. Para un rol nativo, "kind": "native" y groups, con modules y rules vacíos y cluster_scope a true; sin kind, el rol es personalizado. 422 con la primera regla que incumple (códigos role.*); 409 role.exists si el id es de un rol, de un rol borrado o de un rol de sistema.
PUT /api/roles/{id} Admin Edita un rol personalizado: los mismos campos sin id, más la revision que leíste → el rol con revision + 1. 409 role.stale_revision si alguien lo guardó antes; 409 role.cluster_scope_in_use o 409 role.cluster_in_use si el cambio dejaría permisos activos sin efecto; 409 role.psa_in_use si el cambio hace que el rol exija Pod Security mientras tiene permisos activos en namespaces (revócalos y vuelve a concederlos, lo que comprueba la etiqueta de cada namespace); 400 role.system para un rol de sistema; 409 role.kind_immutable si cambia kind. Los clusters donde el rol tiene permisos activos se reconcilian en el acto.
DELETE /api/roles/{id} Admin Borra un rol personalizado → 204. 409 role.in_use mientras tenga permisos activos; con ?revoke_grants=true los revoca en el mismo paso. El id no se reutiliza nunca.
POST /api/roles/preview Admin El mismo cuerpo que POST, sin guardar: {"effective_rules", "violations", "sensitive", "requires_psa", "yaml", "impact", "diff"}. Un borrador no válido no es un error: sus problemas vienen en violations, cada uno con code, params, module, rule (-1 para el propio rol) y field (el campo al que apunta, "" si ninguno). impact y diff comparan con el rol guardado del mismo id. Para un borrador nativo, effective_rules va vacío, sensitive a true, requires_psa a false y yaml es "", e impact lleva además native_clusters: [{"cluster_id", "reachable", "privileged_groups"}], los grupos del borrador a los que cada cluster da acceso privilegiado, de la lectura de cinco minutos.
GET /api/role-modules Admin {"modules": [...]}: el catálogo de capacidades, cada una con id, domain, rules, sensitive, requires_psa, cluster_only y reads (la capacidad de lectura que una de escritura presupone, "" si ninguna).

Un permiso puede nombrar un rol personalizado en POST /api/grants. Además de las comprobaciones de un rol de sistema: 422 grant.unknown_role para un rol desconocido o borrado, 422 grant.role_not_on_cluster si el rol está limitado a otros clusters, 422 grant.role_namespace_only para el ámbito * en un rol que no se puede conceder a todo el cluster, y 409 grant.cluster_bootstrap_v1 si el cluster aún tiene el manifiesto de bootstrap anterior. Un rol nativo responde además 422 grant.role_cluster_only con un ámbito de namespace. Cuando el rol es nativo, puede ser privilegiado por sí mismo (developer, debugger, admin, secrets-reader o un rol personalizado que ejecuta cargas de trabajo o lee o escribe Secrets), o la plantilla de identidad del cluster no es la de por defecto, conceder lee en vivo el RBAC del cluster: 422 grant.privileged_ttl si un nombre que da el permiso tiene acceso privilegiado (un grupo del rol, el nombre de la persona o el grupo de kubelatch para el rol en su ámbito) y falta la caducidad o pasa de 8 horas, 409 grant.identity_collision si la plantilla da a la persona un nombre que ya tiene otra cuenta allí, y 502 cluster.unreachable si el cluster no se puede leer. Lo mismo vale para POST /api/grants/batch.

Credenciales

Método Ruta Acceso Qué hace
GET /api/credentials Sesión, token de la CLI Inventario: un admin ve todas; una persona, las suyas. Filtros: subject (un uuid; un admin elige a cualquiera, el resto solo a sí mismo: 403 para otro sujeto) y active=true para solo las activas ahora (ni revocadas ni caducadas). Cada una lleva su kind: standard, cli (la sesión de un kubelatch login) o ci (emitida por el canje de GitHub Actions). Una credencial ci lleva trust_rule_id, la regla de confianza con la que se emitió (null en las demás, y cuando se borra la regla). Con un token de la CLI, solo las tuyas, aunque seas admin.
POST /api/credentials Sesión Emite una credencial: {"ttl", "name", "note", "cluster_id", "subject_id"}. ttl es obligatorio (7d, 12h). Sin cluster_id, vale en todos los clusters donde el sujeto tenga permisos. Solo un admin emite para otro sujeto. Devuelve token y kubeconfig una sola vez.
POST /api/credentials/{id}/revoke Sesión (dueño o admin), token de la CLI Revoca una credencial. Cuerpo opcional {"reason"}.
POST /api/credentials/lookup Admin Encuentra la credencial a la que pertenece un token entero: {"token": "klt_…"} → la credencial, en cualquier estado (activa, caducada o revocada). El token de un agente delegado también coincide, con kind agent, que ninguna lista devuelve. El token va en el cuerpo, nunca en la URL; se calcula su hash y no se guarda ni se registra. El evento de control credential.lookup lleva matched: true y nombra la credencial si hay coincidencia; una búsqueda sin resultado también deja uno, con matched: false y sin destino. 400 credential.bad_token si no es un token de kubelatch, 404 credential.unknown si ninguna credencial de esta instancia coincide, 429 auth.too_many_attempts pasadas 10 búsquedas por minuto e IP.

CLI de kubelatch

Método Ruta Acceso Qué hace
POST /api/cli/authorizations Anónimo Empieza un kubelatch login: {"kind": "loopback", "code_challenge", "port", "host"} → {"id", "expires_in"}, o {"kind": "device", "host"} → {"id", "device_code", "user_code", "verification_url", "expires_in", "interval"}. 10 por minuto e IP.
POST /api/cli/token Anónimo El canje: {"id", "code", "code_verifier"} o {"device_code"} → {"token", "credential_id", "expires_at", "login"}, una sola vez. Antes de la aprobación, 400 cli.pending; si pregunta más rápido que interval, 429 cli.slow_down; caducada, rechazada o ya usada, 410 cli.expired. 60 por minuto e IP; por encima, 429 auth.too_many_attempts.
GET /api/cli/authorizations/{id} Sesión Una petición del navegador pendiente, para su página de aprobación: {"id", "kind", "host", "request_ip", "created_at", "expires_at", "session_ttl_seconds", "same_network"}. same_network es true cuando la CLI pidió desde la dirección del navegador que aprueba o desde su misma /24 (IPv4) o /64 (IPv6): el único dato que una persona puede contrastar contra un intento de phishing. Es false cuando la petición del navegador trae X-Forwarded-For o Forwarded de un proxy que KUBELATCH_TRUSTED_PROXIES no lista, porque entonces las dos direcciones serían las del proxy.
POST /api/cli/authorizations/{id}/approve Sesión La aprueba: {"state"} → {"redirect"}, siempre http://127.0.0.1:<puerto>/callback?code=…&state=….
POST /api/cli/authorizations/{id}/deny Sesión La rechaza: {"state"} → {"redirect"} (…?error=access_denied, o "" si es de dispositivo).
POST /api/cli/authorizations/lookup Sesión Una petición de dispositivo pendiente por su código: {"user_code"} → la misma vista, same_network incluido. Los códigos erróneos tienen un límite por persona y por IP.
POST /api/cli/authorizations/approve Sesión Aprueba una petición de dispositivo: {"user_code"} → 204.
GET /api/cli/access Sesión, token de la CLI Tus clusters para el kubeconfig: {"server", "ca", "clusters": [{"id", "name", "server", "grants": [{"role", "tier", "scope", "expires_at"}]}]}. tier es el alias obsoleto de role.
POST /api/cli/logout Token de la CLI Revoca la credencial con la que se llama → 204.

Agentes de IA

Las sesiones y solicitudes de los agentes de IA (las rutas y los campos llaman request a una solicitud), y el consentimiento de una persona a un agente delegado. Un administrador ve las de todas las cuentas; los demás, las suyas (404 para las de otra persona). Ninguna de estas rutas admite token de la CLI: decidir y consentir exigen una sesión del navegador. Ver Agentes de IA.

Método Ruta Acceso Qué hace
GET /api/agents/sessions Sesión Sesiones de agente que puedes ver: un administrador, las de todas las cuentas, o las de una con subject (un uuid); los demás, las suyas (403 agent.own_only si filtran por otra cuenta). Sin state, o con state=active, todas las sesiones activas, la usada más recientemente primero: {"sessions": [...], "ended_count", "truncated"}; ended_count es cuántas sesiones terminadas puedes ver y truncated es true cuando hay más de 1000 activas y la lista se corta ahí. Con state=ended, una página de las terminadas, la que terminó más recientemente primero: {"sessions": [...], "page", "page_size", "has_more", "total"}, de 50 en 50 (page, de 1 a 10000), con total el de todas las terminadas que puedes ver; cualquier otro state es 400 query.state. Cada sesión lleva id, mode (bot o delegated), subject, client_name, client_id, writes_need_approval, created_at, expires_at, last_used_at, revoked_at, revoked_by_login, state (active, revoked, expired), credential_prefix, pending_requests, can_cut, follow (none, viewer o same: lo que la sesión toma de los permisos que su persona reciba después; none en la de un bot) y, en una sesión delegada, ceiling. En Pro, cada sesión activa lleva además activity, lo que hizo en las últimas 24 horas: calls (sus filas de auditoría), denied (las que respondieron 401 o 403), hourly (sus llamadas por hora UTC: 24 números, la más antigua primero y la última la hora en curso; la hora parcial anterior cuenta solo en calls) y last_call (at, tool, verb, resource, subresource, namespace, cluster_id y status, 0 mientras la petición sigue en curso; null si no hizo ninguna). Solo para administradores, lleva también worth_a_look, por qué su agente merece un vistazo (un agente es un bot, o un cliente que una persona autorizó; cada una de sus sesiones activas lleva la misma lista, [] si nada lo merece), el más reciente primero: {"kind", "at", "count", …}, con at cuándo se alimentó el motivo por última vez y count su cifra. kind es flood (20 solicitudes o más en la última hora, las rechazadas incluidas: baseline es lo que el agente suele pedir por hora, ausente si no hay historial, y refused), sensitive (en el último día, un acceso claramente sensible pedido y no concedido, uno por cluster_id, namespace y role: denied, expired y refused dicen cómo acabaron las solicitudes), insistence (en el último día, tres solicitudes o más iguales a una que falló, una por solicitud repetida: cluster_id, namespace y role en un acceso, o write con method, path, namespace, resource y name; también denied, expired y refused) o denied (20 llamadas o más que el cluster denegó en la última hora: calls son todas sus llamadas de esa hora). Un motivo flood, sensitive o insistence lleva además refusal (code y params, como el de una solicitud) cuando kubelatch rechazó solicitudes de las que lo componen: la última, y en un flood solo si todas comparten el código. Los campos que no son de un tipo se omiten. activity y worth_a_look son null en Free (worth_a_look también para quien no es admin), en una sesión terminada y en las rutas que devuelven una sola sesión.
GET /api/agents/sessions/{id} Sesión (dueño o admin) Una sesión, con grants: los permisos en vigor ahora.
GET /api/agents/sessions/{id}/events Sesión (dueño o admin) Su línea de tiempo: las filas de auditoría de la sesión, de 50 en 50, las más recientes primero (page). La única ruta en la que alguien que no es admin lee filas de auditoría, y solo de sus propias sesiones. En Free y en instancias caducadas se sirve 24 horas hacia atrás, y la página lleva floor como los listados de auditoría.
DELETE /api/agents/sessions/{id} Sesión (dueño o admin) Corta la sesión: revoca sus credenciales y refresh tokens, cierra sus solicitudes pendientes y revoca los permisos que recibió a petición. 200 con la sesión.
POST /api/agents/sessions/cut Sesión (dueño o admin) Corta varias sesiones a la vez, todas o ninguna: {"ids": [...]}, de 1 a 50 ids (si no, 400 agent.cut_batch_size; un id repetido se corta una vez). Cada sesión se corta como lo hace DELETE, en una sola transacción, y su evento agent.session.revoke lleva el lote. Responde {"sessions": [...]}, cada una como responde DELETE, en el orden pedido. 404 agent.session_not_found si algún id no existe, no es un uuid o no es una sesión que puedas ver, sean como sean los demás: no nombra ningún id y nunca dice que uno existe. 409 agent.cut_batch, con params.session, para la primera sesión que ya no está activa (ya cortada o caducada): no se corta ninguna.
GET /api/agents/requests Sesión Solicitudes, como mucho 200, primero las pendientes: id, kind (write o access), status (pending, approved, denied, expired, executed, refused), session_id, session_mode, session_client_name (el nombre que el agente se da a sí mismo, vacío si no dio ninguno), subject, cluster_id, tool, reason, created_at, expires_at, decided_by_login, decided_at, executed_at, execution_status, execution_audit_id, result_grant_id, can_decide, refusal, y write (method, path, namespace, resource, name) o access (namespace, role, minutes). refused es una solicitud que la política de kubelatch rechazó cuando el agente la hizo (un acceso por encima del rol de su persona, un rol que no se puede pedir, una escritura que las reglas del bot no permiten o una sesión que ya tiene 20 solicitudes esperando): ninguna persona la decide, nunca está pendiente y el agente recibió el mismo error de siempre. refusal es el {"code", "params"} de ese error, que la interfaz traduce como un error de la API; null salvo que status sea refused. Filtros: status=pending, session (un uuid).
GET /api/agents/requests/{id} Sesión (dueño o admin) Una solicitud, con los campos de la lista; la de una escritura lleva además preview (current y result, con los valores de los Secrets ocultos: cada uno muestra su tamaño y, en result, si la escritura lo cambia).
POST /api/agents/requests/{id}/approve Sesión (quien decide) La aprueba. La de un bot: cualquier admin; la de una sesión delegada: solo su persona (403 agent.not_decider para cualquier otro que pueda verla). Cerrada: 409 agent.request_closed.
POST /api/agents/requests/{id}/deny Sesión (quien decide) La deniega.
POST /api/agents/requests/approve Sesión (quien decide) Aprueba varias a la vez, todas o ninguna. Cuerpo {"ids": [...]}, de 1 a 20 ids de escrituras pendientes de una misma forma: la misma sesión de agente, cluster, tool, recurso y namespace (422 agent.batch_shape si no, también para una solicitud de acceso; 400 agent.batch_size por la cantidad). Cada id pasa las comprobaciones de la aprobación de una; la primera que falla se nombra en params.index con ese error (404, 403 agent.not_decider, 409 agent.request_closed) y no se decide nada. Responde {"requests": [...]}, cada una como GET /api/agents/requests/{id}, en el orden pedido.
POST /api/agents/requests/deny Sesión (quien decide) Deniega varias a la vez, todas o ninguna: el mismo cuerpo y los mismos errores, para cualquier solicitud pendiente que la persona pueda decidir, escrituras y accesos por igual.
GET /api/agents/authorizations/{id} Sesión Un inicio de sesión pendiente de un agente delegado, para la página de consentimiento: id, client (name, origin, verified_host, redirect_uri, redirect_host, redirect_remote, loopback_only), grants (cada uno con sus choices y su source, como en GET /api/grants: directo, o el grupo del que viene), default_ttl_seconds, max_ttl_seconds, expires_at. redirect_remote es true salvo que la respuesta de esta petición vaya a una dirección loopback de este ordenador (http o https en localhost, 127.0.0.0/8 o [::1]): cualquier otro esquema cuenta, porque la aplicación que lo abre puede reenviar la respuesta.
POST /api/agents/authorizations/{id}/approve Sesión {"grants": [{"grant_id", "role"}], "ttl_seconds", "writes_need_approval", "follow"} → {"redirect"}: adónde vuelve el navegador al agente, con el código. follow (none, viewer o same) es lo que la sesión toma de los permisos que la persona reciba después; ausente vale none, para que un cliente antiguo falle cerrado; cualquier otro valor, 400 oauth.bad_follow.
POST /api/agents/authorizations/{id}/deny Sesión → {"redirect"} con error=access_denied.

Las tres rutas de authorizations existen solo mientras KUBELATCH_MCP_ENABLED es true.

Edición

Todas requieren admin.

Método Ruta Qué hace
GET /api/edition La edición en vigor: edition (free o pro), status (free, trial, pro, pro_grace, lapsed), source de la clave (none, env, ui), invalid (por qué la clave no verifica, vacío si verifica), customer, license_id, seats (0 es sin límite), trial, expires_at, grace_until, over_seats (más personas habilitadas que plazas tiene la clave; se frena una más), trial_until (cuándo acaba o acabó la única prueba de esta instancia; null si no la hubo), limits (clusters, people, custom_roles, retention_days; null es sin límite, y en retention_days significa la retención configurada), usage, audit_retention_hours, warnings (expiring, grace, lapsed, over_seats, invalid_env_license, license_invalid, people_at_limit, clusters_at_limit) y version. Nunca la clave. audit_retention_hours es cuánto tiempo guarda ahora la auditoría el trabajo de retención: KUBELATCH_AUDIT_RETENTION, o 24 en una instancia que nunca tuvo clave cuando esa retención es más larga o ilimitada; 0 no borra nada. version es lo que sabe la comprobación de versión diaria: {"current", "latest", "latest_url", "checked_at", "enabled", "update_available"}, con latest, latest_url y checked_at vacíos (null en la fecha) hasta que una comprobación acierta, y enabled a false si la comprobación está apagada. Es null en un servidor sin comprobador.
PUT /api/edition/license Instala una clave ({"token"}), guardada cifrada; responde la edición nueva, version incluida. 422 edition.license_invalid con el motivo; 422 edition.trial_used para una clave de prueba de otra suscripción distinta de la única prueba que tuvo esta instancia (params.until es el día en que acaba esa prueba); 409 edition.license_from_env mientras KUBELATCH_LICENSE esté puesta.
DELETE /api/edition/license Quita la clave instalada (204); 409 edition.license_from_env mientras KUBELATCH_LICENSE esté puesta.
POST /api/edition/version-check Comprueba la versión en el momento y responde el objeto version de GET /api/edition. 409 edition.update_check_off con la comprobación desactivada, 502 edition.update_check_failed cuando el sitio no responde (se conserva el resultado anterior), 404 en un servidor sin comprobación. Solo comprueba la réplica que responde.

GET /api/me lleva edition (free o pro) para todo el mundo. Un alta por encima de un límite (POST /api/subjects para una persona, POST /api/subjects/{id}/enable sobre una persona, POST /api/clusters, POST /api/roles y PUT /api/roles/{id}, salvo para un rol nativo) responde 422 edition.limit con resource, max y edition. En instancias Free y caducadas, GET /api/audit, GET /api/audit/summary y GET /api/control-events sirven las últimas 24 horas y llevan floor, el instante más antiguo servido (null si no hay recorte); GET /api/audit/{id} responde 404 para un evento más antiguo, igual que para un id desconocido.

Webhooks

Todas requieren admin. Los webhooks son de Pro: fuera de Pro, crear, cambiar y probar uno responde 403 edition.feature, mientras que listarlos, consultar sus entregas y borrarlos funciona en cualquier edición.

Método Ruta Qué hace
GET /api/webhooks {"webhooks": [...]}, del más antiguo al más reciente. Cada uno: id, name, origin (el esquema y el host de la URL, https://hooks.slack.com: la URL en sí no se puede volver a leer, porque su ruta y su query llevan la credencial de Slack y de Teams), format (json, slack o teams), locale (en o es), events (en el orden grant.expiring, break_glass.login, join_request.created, version.available), active, created_at, updated_at y last_delivery, su entrega más reciente tal como la da /deliveries (null antes de la primera). Nunca la URL ni el secreto, ni aquí ni en otra respuesta: los dos son de solo escritura, y el secreto llega una sola vez, en la respuesta de la creación.
POST /api/webhooks Crea uno: {"name", "url", "format", "locale", "events"} → 201 con {"webhook", "secret"}. secret (whsec_…, la clave de la firma) solo viene en esta respuesta. Un locale vacío es en; un evento repetido cuenta una vez.
PATCH /api/webhooks/{id} Cambia cualquiera de name, url, format, locale, events y active (false lo pausa, true lo reanuda); un campo que no se manda se queda como está → {"webhook"}. url sustituye la dirección, que entonces se vuelve a comprobar; sin url, se queda la de siempre.
DELETE /api/webhooks/{id} Lo borra con sus entregas → 204.
POST /api/webhooks/{id}/test Manda un ping en el momento, esté pausado o no, y lo intenta una vez → 200 con {"delivery"}, delivered o failed: un receptor que falla también da aquí un 200.
GET /api/webhooks/{id}/deliveries {"deliveries": [...]}, de la más reciente a la más antigua, como mucho 50. Cada una: id (la cabecera Kubelatch-Delivery), event (uno de los cuatro, o ping), status (pending, delivered, failed, skipped o skipped_edition, que es omitida porque terminó Pro), attempts (los POST hechos, el que acertó incluido), response_code (null sin respuesta), last_error, created_at, next_attempt_at (solo mientras está pending; null si no) y delivered_at. Nunca el cuerpo enviado.

404 webhook.not_found para un id desconocido. Un cuerpo que no pasa responde 400: webhook.bad_name (de 1 a 80 caracteres sin caracteres de control, params.max), webhook.bad_url (no es una URL https con host, sin usuario ni fragmento, de 2.048 bytes como mucho), webhook.private_url (el host es localhost o una dirección que no es pública, salvo que KUBELATCH_WEBHOOK_ALLOW_PRIVATE sea true), webhook.bad_format, webhook.bad_locale o webhook.bad_events (ninguno, o uno que no es de los cuatro).

Auditoría

Método Ruta Acceso Qué hace
GET /api/audit Sesión Peticiones al proxy, de 50 en 50. Filtros: subject, cluster, namespace, verb, resource, credential, token_prefix (klt_ seguido de 8 caracteres; también encuentra peticiones rechazadas antes de resolverse a un id de credencial), agent_session (el id de una sesión de agente) y mcp_tool (la herramienta MCP que hizo la petición), from y to (RFC 3339), include_discovery=true, outcome, access, environment y production (solo admins: ver abajo), q (parte del nombre con el que la petición entró en el cluster, impersonated_user; 253 caracteres como máximo), page (1 a 10000). Sin rol de admin, solo las propias.
GET /api/audit/summary Sesión Recuento de lo que /api/audit listaría con los mismos filtros (page se ignora): total, denied, errors, writes, agent_calls (peticiones hechas por una herramienta MCP), by_cluster (cluster_id, total, denied, errors; primero el de más peticiones) y buckets (start, total, denied, errors; del más antiguo al más reciente, todos los tramos del periodo presentes, a cero si están vacíos). Sin from ni to cuenta las últimas 24 horas; con from solo, hasta ahora; con to solo, las 24 horas anteriores. Los tramos son horas UTC hasta 48 horas y días UTC a partir de ahí. Un periodo de más de 3660 días se recorta a los 3660 días anteriores a to. Responde from, to y floor (RFC 3339; floor como en los listados), así que el periodo contado siempre es explícito. Sin rol de admin, solo las propias.
GET /api/audit/{id} Sesión Una petición por su Audit-ID. La de otra persona responde 404 si no eres admin. En instancias Free y caducadas, la anterior a floor responde 404 a cualquiera, admins incluidos, igual que un id desconocido. Además de los campos de la fila lleva grants: los permisos de la cuenta de la petición en su cluster que estaban vigentes cuando se hizo (creados antes, ni revocados ni caducados en ese momento) y cuyos grupos de Kubernetes llevaba la petición, cada uno {"id", "role", "scope", "note", "ticket", "source": "direct"\|"group"\|"set", "group_id"?, "set_grant_id"?}; [] para una petición sin cuenta. Sin rol de admin solo ves las tuyas, así que solo tus permisos.
GET /api/control-events Admin Acciones del plano de control e intentos de login, de 50 en 50. Filtros: actor (uuid), action (coincidencia exacta, un código de minúsculas, _ y ., de hasta 64 caracteres, p. ej. user.create), cluster (un id de cluster: sus eventos y los de los permisos que hay en él), from y to (RFC 3339), page (1 a 10000).
GET /api/insights Admin Lo que leen el «Inicio» de un administrador y la página «Operativa» además de sus listas. sensitive: los exec y attach (exec), las lecturas de Secrets con get, list o watch (secrets) y los port-forward (portforward) de las últimas 24 horas, permitidos y denegados, por tipo y por cuenta (kind, subject_id, login, count; por tipo, primero el que más). sensitive_scope dice qué clusters cuentan: all, todos, o production, desde que un entorno se marca como producción, cuando cuentan solo los clusters de los entornos marcados (uno sin clusters no cuenta nada) y GET /api/audit?production=true lista las filas de cada recuento. GET /api/insights/operations cuenta todos los clusters en ambos casos. unused_grants: los permisos activos creados hace más de 30 días cuya cuenta no ha mandado ninguna petición a su cluster (ni a su namespace, salvo con *) en los últimos 30 días, una fila por permiso, los de un grupo una por miembro (grant_id, last_used_at: su última petición anterior, null si la auditoría no guarda ninguna). expiring_share: por cada día UTC del periodo, los permisos activos al acabar el día (ahora, para hoy) y cuántos de ellos tienen fin (day, active, with_end), reconstruidos con las fechas de los permisos. sensitive_share: lo mismo ahora para los roles marcados como sensibles (active, with_end). account_access: las cuentas activas con algún permiso en vigor, según el tipo de credencial que usaron en el periodo, cada una en un solo grupo y primero la larga duración: las personas, por una credencial estándar de larga duración (long), una sesión de la CLI (cli), un agente delegado que actuó por ellas (agent) o nada (none); los bots, por una credencial estándar (long), un token de CI (ci) o nada (none); los agentes, uno por cuenta y nombre de cliente con alguna sesión activa ahora o usada en el periodo, por una sesión de un bot (bot), una sesión delegada de una persona (delegated) o ninguna usada (none). Lee las credenciales y las sesiones, no la auditoría, así que floor no la recorta. period es 7, 30 (por defecto) o 90 días y solo mueve expiring_share y account_access; cualquier otro valor es 400 query.bad_period. Responde from y to (el periodo) y floor, el más reciente entre el suelo de la edición y lo que guarda la retención de la auditoría (null si ninguno recorta); si cae dentro de los 30 días, unused_grants va vacío y unused_grants_floor lo lleva (null si no).
GET /api/insights/operations Admin Lo que enseña la página «Operativa» sobre la operación de los clusters en el periodo. Una petición cuenta si tiene recurso o se rechazó (401 o 403). accounts: una fila por cuenta, primero la que más peticiones tiene (kind: person, bot, agent, o none para un token desconocido, revocado o caducado; subject_id y login, en un agente los de su persona o su bot, con su client; requests, denied, sensitive, deletes y last_at), cortada en 500 filas pero con todas las anomalías (anomaly: denied, sensitive o vacío; score: cuánto pasa de su umbral); accounts_total, sensitive_accounts y denied (todas las peticiones rechazadas del periodo) cuentan antes del corte. by_day: las peticiones por tipo de cuenta y por día UTC, o por hora si hourly es true (start, person, bot, agent, none, y denied: cuántas de ellas se rechazaron, como las cuenta el denied de una cuenta). by_hour: las peticiones por hora UTC, solo las horas con alguna (start, requests). sensitive: los exec, secrets y portforward del periodo, como los cuenta access en /api/audit. clusters: cada cluster registrado con sus requests, denied y sensitive y sus señales (status, everyone_privileged, bootstrap_version, k8s_version y k8s_unsupported: una versión menor más de tres por detrás de aquella con la que se compiló kubelatch). period es como en GET /api/insights. from es el más reciente entre el inicio del periodo y floor, redondeado hacia arriba a una hora entera si deja 48 horas o menos (hourly), o si no a un día UTC entero; to es ahora.

Por defecto /api/audit oculta las filas sin recurso (descubrimiento de la API y fallos de autenticación); include_discovery=true las muestra. outcome deja una clase de resultado: denied (401 o 403), error (5xx, o un error tras un estado de éxito, como un stream que el cluster cortó) o write (create, update, patch, delete, deletecollection, o un exec, attach o port-forward). Cualquier otro valor es 400 query.bad_filter. Los denied, errors y writes del resumen cuentan exactamente las filas que seleccionan esos tres valores. access deja un tipo de acceso sensible, permitido y denegado por igual: exec (los subrecursos exec y attach), secrets (get, list o watch sobre Secrets) o portforward; cualquier otro valor es 400 query.bad_access. Es la definición de los recuentos sensitive de GET /api/insights, así que cada uno de ellos es el número de filas que selecciona su valor. environment (el id de un entorno) deja las peticiones de los clusters que ese entorno tiene hoy, no de los que tenía cuando se hizo la petición, y production=true las de todos los entornos marcados como producción (ninguno marcado, nada; un entorno de producción sin clusters no deja nada, nunca todos los clusters). Ambos listados los admiten; quien no tiene el rol de admin recibe 403 session.admin_only con cualquiera de los dos, sea cual sea su valor. Un environment que no es el id de un entorno existente, un production distinto de true, o los dos a la vez, es 400 query.bad_environment. Ambos listados responden events, page, page_size, has_more y floor (Edición). Cada petición lleva impersonated_user, el nombre que el proxy mandó al cluster por ella (Impersonate-User), vacío en una petición rechazada antes de calcular ese nombre.

Reglas de confianza de CI

Todas requieren admin.

Método Ruta Qué hace
GET /api/ci/trust-rules Lista las reglas y además la audiencia, el emisor, la duración de los tokens y la ruta de intercambio que debe usar un workflow. Cada regla lleva repository_label (el owner/repo con el que se escribió; vacío si solo se dieron los ids numéricos) y last_redeemed_at (cuándo intercambió un token un workflow con ella por última vez; null si ninguno lo hizo).
POST /api/ci/trust-rules Crea una regla: {"name", "bot_subject_id", "repository", "repository_owner_id", "repository_id", "repository_label", "ref", "environment"}. Nombra el repositorio con repository ("owner/repo"), que la GitHub App resuelve a los ids y a la etiqueta, o con los ids numéricos repository_owner_id y repository_id, más una repository_label opcional (owner/repo, como mucho 140 caracteres) para que la lista la muestre. Con la GitHub App configurada, una repository_label dada junto a los ids también se resuelve y tiene que nombrar esos ids. 400 ci.repository_needs_github si das repository sin la GitHub App configurada, 404 ci.repository_not_found si GitHub no conoce el repositorio o la App no lo ve, 400 ci.repository_moved si GitHub responde con una redirección porque el repositorio cambió de nombre o de dueño (usa su owner/repo actual), 400 ci.github_not_installed si la App está configurada pero no instalada en la organización, 400 ci.bad_repository para un nombre o una etiqueta que no sea owner/repo, o una etiqueta que no nombre los ids dados. ref y environment vacíos valen para cualquiera.
DELETE /api/ci/trust-rules/{id} Elimina una regla. No revoca las credenciales ya emitidas.

Interfaz web

Cualquier otra ruta sirve la SPA embebida: el fichero si existe o index.html para las rutas de la interfaz (/login, /account-link, /account, /activity, /cli/…, /admin/…, /agents/…). Un fichero ausente bajo /assets/ es 404. Solo admite GET y HEAD.