El sanitizer de /actuator/env no conoce tus convenciones

작성자

카테고리:

← 피드로
DEV Community · Juan Torchia · 2026-09-11 개발(SW)

Juan Torchia

Hace poco escribí sobre allowlist en actuator endpoints: qué exponer y qué no. Quedó una pregunta abierta que me siguió picando: si decidís dejar /actuator/env habilitado —porque lo necesitás para debug en staging, porque el equipo de infra lo pide— ¿qué te asegura que no vas a mostrar un secreto en texto plano la primera vez que alguien le pegue un curl?

La respuesta corta es: nada, si confiás ciegamente en el sanitizer default.

El problema con actuator env show values

Spring Boot trae un sanitizer que enmascara automáticamente ciertos valores antes de mostrarlos en /actuator/env. Funciona por nombre de propiedad: si la clave contiene password, secret, key, token o credentials, el valor sale como ******. Es una defensa razonable para el caso genérico.

El problema es justamente eso: es genérica. Cubre las palabras que un desarrollador de Spring imaginó que ibas a usar. No cubre las que tu equipo realmente usa.

Pensá en cómo se nombran las variables de entorno en un proyecto real: DB_PASS en vez de DB_PASSWORD, API_AUTH en vez de API_TOKEN, WEBHOOK_SIGNING, PARTNER_SHARED_VALUE, INTERNAL_CIPHER. Ninguna de esas contiene las palabras clave que el sanitizer busca. Todas terminan en la respuesta JSON de /actuator/env sin ningún asterisco.

Mi tesis es esta: el sanitizer default no falla porque esté mal escrito, falla porque asume que todo el mundo nombra igual. Y en la práctica cada equipo tiene su propio dialecto de variables de entorno. Las fugas reales casi nunca pasan por password mal escrito — pasan por la convención custom que alguien inventó en un sprint y que nadie agregó a la lista de patrones.

Qué dice la fuente oficial y qué no dice

La documentación oficial de Spring Boot Actuator confirma el comportamiento: el endpoint /env aplica sanitización sobre los valores antes de exponerlos, y ese comportamiento es configurable mediante la interfaz SanitizingFunction, que reemplazó al mecanismo de Sanitizer basado en keywords en versiones más recientes del framework.

Lo que la doc no dice —porque no es su trabajo decirlo— es qué nombres específicos vas a usar en tu proyecto. Eso lo tenés que auditar vos. La doc te da el mecanismo de extensión; el catálogo de qué sanitizar es responsabilidad de quien configura el proyecto, no del framework.

Esa diferencia entre “mecanismo disponible” y “configuración correcta para este caso puntual” es exactamente donde se cuelan la mayoría de los incidentes de exposición que se reportan en configuraciones default sin revisión.

Dónde se equivoca la gente

La receta común que veo repetida en foros y en configuraciones heredadas es: “activá management.endpoint.env.show-values=when-authorized y ya estás cubierto”. Eso resuelve quién puede ver los valores, no qué valores se muestran sin máscara a quien tiene autorización. Son dos problemas distintos y se los trata como uno solo todo el tiempo.

El costo oculto aparece cuando el endpoint queda accesible para un rol interno —un servicio de monitoreo, un dashboard de operaciones— y ese rol termina viendo credenciales que nadie pensó en enmascarar porque el nombre de la variable no calzaba con el patrón default.

Caso tipico que uso para explicar esto: un proyecto que usa Vault o AWS Secrets Manager para producción, pero en application-staging.yml deja variables con nombres tipo THIRD_PARTY_SHARED_SECRET_VALUE. El sanitizer default no necesariamente matchea SHARED_SECRET_VALUE como unidad reconocible si el patrón de búsqueda es más rígido que una simple substring de “secret” — y en configuraciones custom heredadas, es común que las variantes con guiones bajos o mayúsculas mixtas queden afuera si alguien sobreescribió el regex sin revisarlo a fondo.

Acá va la forma de extender el sanitizer con un patrón propio, usando SanitizingFunction:

// Configuracion custom del sanitizer para /actuator/env
@Bean
public SanitizingFunction customSanitizingFunction() {
    // Patron que cubre convenciones propias del equipo
    Pattern patronCustom = Pattern.compile(
        "(?i).*(pass|auth|signing|shared|cipher).*"
    );
    return data -> {
        String nombre = data.getSanitizableData().getKey();
        if (patronCustom.matcher(nombre).matches()) {
            return data.withValue("******");
        }
        // si no matchea, dejamos que el sanitizer default siga la cadena
        return data;
    };
}

Enter fullscreen mode Exit fullscreen mode

Este bean se agrega a la cadena existente de sanitizers; no la reemplaza. Spring Boot ejecuta todos los SanitizingFunction registrados en orden y aplica el enmascarado si alguno de ellos decide que corresponde.

flowchart LR
  A[Request a /actuator/env] --> B{Sanitizer default}
  B -->|nombre matchea password/secret/token| C[Enmascarado con ******]
  B -->|no matchea| D{SanitizingFunction custom}
  D -->|nombre matchea patron propio| C
  D -->|no matchea nada| E[Valor expuesto en texto plano]

Ese último camino, el de la derecha, es el que hay que cerrar activamente. No se cierra solo.

Matriz de decisión para /actuator/env

Situación Qué mirar primero Qué hacer Endpoint expuesto solo en localhost/debug Confirmar que no hay tunneling ni proxy hacia afuera Sanitizer default puede alcanzar, pero auditá nombres de variables igual Endpoint accesible en staging con roles de monitoreo Qué variables custom usa tu equipo, no solo las de Spring Agregar SanitizingFunction con el patrón propio antes de habilitar acceso Variables con nombres no convencionales (DB_PASS, API_AUTH) Listar todas las claves de application.yml y .env de cada ambiente Extender el patrón regex, no confiar en la lista default Integraciones con proveedores externos (webhooks, partners) Nombres que el partner define, no los que definís vos Sanitizer custom por prefijo o sufijo del proveedor Necesitás decidir si deshabilitar el endpoint entero Si nadie en el equipo audita nombres de propiedades regularmente Deshabilitarlo es más seguro que un sanitizer sin mantenimiento

La última fila es la que más me interesa señalar: si no hay alguien revisando periódicamente qué nombres de propiedad aparecen en el código, un sanitizer custom mal mantenido da una falsa sensación de seguridad — peor que no tener nada, porque genera confianza donde no debería haberla. Prefiero el criterio de allowlist que ya planteé en el post anterior antes que un sanitizer que nadie actualiza.

Errores comunes y gotchas

  • Confundir autorización con sanitización. show-values=when-authorized controla acceso, no contenido. Son configuraciones independientes que hay que revisar las dos.
  • Copiar el regex de un proyecto anterior sin adaptarlo. Las convenciones de nombres cambian entre equipos y entre proyectos dentro del mismo equipo.
  • No testear el sanitizer con un caso negativo. Falta común: nadie escribe un test que verifique que una variable con nombre “raro” efectivamente sale enmascarada.
  • Pensar que el problema es solo de producción. Staging y entornos de desarrollo compartidos también exponen /actuator/env, y ahí suelen vivir credenciales reales de servicios de terceros para pruebas de integración.
  • Asumir que el framework te cubre las espaldas para siempre. Spring Boot mantiene actualizados sus propios keywords default con cada release; la lista de convenciones custom de tu equipo no forma parte de esa actualización — ese mantenimiento es tuyo, siempre.

FAQ

¿El sanitizer de Spring Boot enmascara todos los valores sensibles por default?
No. Enmascara valores cuyo nombre de propiedad contiene palabras específicas como password, secret, key, token o credentials. Cualquier convención de nombres distinta a esa queda sin cubrir salvo que se configure explícitamente.

¿Qué diferencia hay entre Sanitizer y SanitizingFunction?
SanitizingFunction es la interfaz recomendada en versiones recientes de Spring Boot para extender el comportamiento de sanitización de forma programática, reemplazando el enfoque anterior basado únicamente en una lista de keywords fija.

¿Puedo tener varios SanitizingFunction registrados a la vez?
Sí. Spring Boot los ejecuta en cadena; si cualquiera de ellos decide enmascarar un valor, ese valor queda enmascarado en la respuesta final.

¿Deshabilitar /actuator/env es más seguro que sanitizarlo?
Depende del caso de uso. Si tu equipo no tiene proceso para mantener actualizado el sanitizer custom, deshabilitar el endpoint o restringirlo con una allowlist estricta reduce el riesgo con menos mantenimiento continuo.

¿show-values=when-authorized resuelve el problema de los secretos expuestos?
No por sí solo. Controla quién puede ver los valores, no qué valores se muestran sin máscara a esos usuarios autorizados. Son dos configuraciones distintas que hay que combinar.

¿Cómo pruebo que mi sanitizer custom funciona antes de deployar?
Con un test unitario que invoque el bean SanitizingFunction directamente contra nombres de propiedad reales de tu proyecto, incluyendo casos con guiones bajos, mayúsculas mixtas y prefijos de proveedores externos.

Mi postura

El sanitizer default de Spring Boot no es un placebo: cubre el caso genérico razonablemente bien, según lo que documenta la propia referencia oficial. Lo que no puedo sostener sin evidencia productiva es cuánto exactamente reduce el riesgo en un proyecto específico — eso depende enteramente de qué tan alejadas estén las convenciones de nombres de ese proyecto respecto de la lista default.

Lo que sí puedo afirmar con criterio técnico: si nadie auditó los nombres de propiedad custom del proyecto contra la lista de patrones del sanitizer, hay una brecha sin cerrar. No es una posibilidad remota, es una consecuencia directa de cómo funciona el mecanismo.

Mi recomendación práctica es la misma lógica que usé para pensar allowlist de endpoints en el post anterior sobre actuator: tratá la sanitización como una lista viva que se revisa cada vez que se agrega una integración nueva, no como una configuración que se pone una vez y se olvida. El regex de hoy no cubre el nombre de variable que alguien va a inventar en el próximo sprint.

Lo incómodo de todo esto es que no hay checklist que te salve si nadie mira. Podés tener el SanitizingFunction más prolijo del mundo y seguir filtrando un secreto porque el pasante que armó la integración con el partner nuevo usó PARTNER_KEY_RAW y a nadie se le ocurrió actualizar el regex esa semana. La pregunta que me hago antes de habilitar cualquier endpoint de actuator no es “¿tiene sanitizer?” sino “¿quién es responsable de actualizar esta lista cuando cambie algo?”. Si no hay respuesta clara, prefiero apagarlo.

Si te interesa cómo pienso decisiones de arquitectura con este mismo criterio de “qué cubre la herramienta versus qué tengo que cubrir yo”, tengo posts relacionados sobre JWT vs sesiones con estado y sobre qué significa el camino a Java Champion que tocan la misma tensión desde otros ángulos.

Fuente original:

Este artículo fue publicado originalmente en juanchi.dev

원문에서 계속 ↗