AGENTFLOW Quarkus 3.x · Java 21 MicroProfile JWT · Keycloak OIDC Producción

Guía de Seguridad — API Backend

Autenticación OIDC/JWT, RBAC, validación de entrada, cabeceras HTTP, rate limiting, gestión de secretos y auditoría para la API REST de AgentFlow.

🗺️
Mapa de Endpoints y Roles
AgentFlow API v2 — control de acceso por rol
ADMIN
  • Todas las operaciones
  • Gestión de usuarios
  • Eliminar agentes
  • Configuración global
AGENT_MANAGER
  • Crear/editar agentes
  • Listar agentes
  • Ver métricas
  • No eliminar usuarios
VIEWER
  • Ver agentes (solo lectura)
  • Ver ejecuciones
  • Sin escritura
  • Sin admin
Método Ruta Rol mínimo Notas
GET /api/agents VIEWER Paginado, filtrado seguro
POST /api/agents AGENT_MANAGER Bean Validation completa
DELETE /api/agents/{id} ADMIN Soft delete + auditoría
GET /api/admin/users ADMIN Solo ADMIN, siempre auditado
POST /api/auth/login público Rate limit: 5 req/min por IP
🔐
Autenticación OIDC/JWT
Keycloak como Identity Provider, MicroProfile JWT
JWT OIDC application.properties
ℹ️ Configuración Keycloak: AgentFlow usa realm agentflow-prod con client ID backend-api. El secreto del cliente nunca debe aparecer en el repositorio.
properties application.properties
# ── OIDC: Keycloak realm agentflow-prod ── quarkus.oidc.auth-server-url=https://auth.agentflow.io/realms/agentflow-prod quarkus.oidc.client-id=backend-api quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET} # ← env var, nunca hardcoded quarkus.oidc.token.issuer=https://auth.agentflow.io/realms/agentflow-prod # ── MicroProfile JWT ── mp.jwt.verify.publickey.location=META-INF/resources/keycloak-public.pem mp.jwt.verify.issuer=https://auth.agentflow.io/realms/agentflow-prod # ── SmallRye JWT (claim mapping) ── smallrye.jwt.path.groups=realm_access/roles # Keycloak pone roles aquí
java AgentResource.java
@Path("/api/agents") @Authenticated // ← todo el recurso requiere JWT válido @Produces(MediaType.APPLICATION_JSON) public class AgentResource { @Inject JsonWebToken jwt; @Inject SecurityIdentity identity; @GET @RolesAllowed({"VIEWER", "AGENT_MANAGER", "ADMIN"}) public Response listAgents() { String owner = jwt.getName(); // sub del token Set<String> roles = jwt.getGroups(); return Response.ok(Map.of( "user", owner, "roles", roles, "agents", agentService.listForUser(owner, identity) )).build(); } @DELETE @Path("/{id}") @RolesAllowed("ADMIN") public Response deleteAgent(@PathParam("id") Long id) { agentService.softDelete(id); return Response.noContent().build(); } }
Validación de Entrada
Bean Validation + validadores personalizados para POST /api/agents
SIN validar ✗ CON validar ✓
java CreateAgentDto.java
// ✗ MAL — sin validación, cualquier payload pasa public record CreateAgentDto(String name, String prompt, String model) {} // ✓ BIEN — validación completa con Bean Validation public record CreateAgentDto( @NotBlank @Size(min = 3, max = 80) @Pattern(regexp = "^[\\w\\s\\-áéíóúÁÉÍÓÚñÑ]+$", message = "Nombre solo permite letras, números, espacios y guiones") String name, @NotBlank @Size(max = 4000, message = "El prompt no puede superar 4000 caracteres") String prompt, @NotNull @Pattern(regexp = "^(gpt-4o|claude-3|gemini-1\\.5).*$", message = "Modelo no permitido") String model, @Valid @NotNull AgentConfigDto config // nested validation ) {} // Endpoint que usa el DTO validado @POST @RolesAllowed({"AGENT_MANAGER", "ADMIN"}) public Response createAgent(@Valid @NotNull CreateAgentDto dto) { Agent agent = agentService.create(dto, identity.getPrincipal().getName()); return Response.status(201).entity(agent).build(); }
💡 Tip: Añade un ExceptionMapper<ConstraintViolationException> para devolver errores de validación en formato JSON estructurado en lugar del 400 genérico de Quarkus.
🗄️
Prevención de SQL Injection
Panache con queries parametrizadas — nunca concatenación de strings
java AgentRepository.java
@ApplicationScoped public class AgentRepository implements PanacheRepository<Agent> { // ✗ MAL — nunca concatenar input del usuario en queries public List<Agent> searchBad(String name) { return list("name = '" + name + "'"); // ← VULNERABLE } // ✓ BIEN — parámetro posicional public List<Agent> findByName(String name, String owner) { return list("LOWER(name) LIKE ?1 AND owner = ?2 AND active = true", "%" + name.toLowerCase() + "%", owner); } // ✓ BIEN — named parameters (más legible) public List<Agent> findActiveByModel(String model, String owner) { return list("model = :model AND owner = :owner AND active = true", Parameters.with("model", model).and("owner", owner)); } // ✓ Query nativa segura (necesaria para FTS) public List<Agent> fullTextSearch(String query) { return getEntityManager() .createNativeQuery( "SELECT * FROM agents WHERE to_tsvector('spanish', name || ' ' || prompt) @@ plainto_tsquery('spanish', :q)", Agent.class) .setParameter("q", query) // ← parametrizado, seguro .getResultList(); } }
📋
Cabeceras de Seguridad HTTP
Filter global para todas las respuestas de la API
java SecurityHeadersFilter.java
@Provider @Priority(Priorities.HEADER_DECORATOR) public class SecurityHeadersFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext req, ContainerResponseContext res) { var h = res.getHeaders(); // Clickjacking: AgentFlow es API pura, no necesita frames h.putSingle("X-Frame-Options", "DENY"); // MIME sniffing h.putSingle("X-Content-Type-Options", "nosniff"); // HSTS — 1 año en producción h.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload"); // CSP — API JSON, sin scripts inline h.putSingle("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'"); // Ocultar tecnología h.remove("Server"); h.remove("X-Powered-By"); // Referrer policy h.putSingle("Referrer-Policy", "strict-origin-when-cross-origin"); } }
properties CORS — application.properties
quarkus.http.cors=true quarkus.http.cors.origins=https://app.agentflow.io,https://admin.agentflow.io quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS quarkus.http.cors.headers=accept,authorization,content-type quarkus.http.cors.access-control-allow-credentials=true quarkus.http.cors.access-control-max-age=12H
⏱️
Rate Limiting
Protección brute-force en /api/auth/login — 5 intentos/min por IP real
⚠️ Seguridad: Nunca uses X-Forwarded-For directamente — los clientes pueden falsificarlo. Usa getRemoteAddr() del servlet request, o configura quarkus.http.proxy.proxy-address-forwarding=true si hay proxy de confianza.
java LoginRateLimitFilter.java
@Provider @Priority(Priorities.AUTHENTICATION - 1) // antes de auth public class LoginRateLimitFilter implements ContainerRequestFilter { // 5 intentos por minuto por IP en /api/auth/login private static final int MAX_ATTEMPTS = 5; private static final long WINDOW_MS = 60_000; private final ConcurrentHashMap<String, Deque<Long>> attempts = new ConcurrentHashMap<>(); @Context HttpServletRequest servletRequest; @Override public void filter(ContainerRequestContext ctx) { if (!ctx.getUriInfo().getPath().startsWith("/api/auth/login")) return; String ip = servletRequest.getRemoteAddr(); // IP real (o proxy confiable) Deque<Long> ts = attempts.computeIfAbsent(ip, k -> new ArrayDeque<>()); long now = System.currentTimeMillis(); synchronized (ts) { // Limpiar timestamps fuera de la ventana ts.removeIf(t -> (now - t) > WINDOW_MS); if (ts.size() >= MAX_ATTEMPTS) { ctx.abortWith(Response.status(429) .header("Retry-After", 60) .entity(Map.of("error", "Demasiados intentos. Espera 60 segundos.")) .build()); return; } ts.addLast(now); } } }
🔑
Gestión de Secretos
Variables de entorno en K8s Secrets + HashiCorp Vault para producción
properties application.properties — referencias, nunca valores
# ✓ BIEN — referenciar variables de entorno, nunca el valor real quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:5432/agentflow quarkus.datasource.username=${DB_USER} quarkus.datasource.password=${DB_PASSWORD} quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET} agentflow.openai.api-key=${OPENAI_API_KEY} agentflow.anthropic.api-key=${ANTHROPIC_API_KEY} # Vault (producción) quarkus.vault.url=https://vault.agentflow.io quarkus.vault.authentication.kubernetes.role=agentflow-api quarkus.vault.secret-config-source.path=agentflow/prod/secrets
java LLMProviderService.java — inyección segura de API keys
@ApplicationScoped public class LLMProviderService { // Las keys llegan desde Vault o env vars, nunca hardcoded @ConfigProperty(name = "agentflow.openai.api-key") String openAiKey; @ConfigProperty(name = "agentflow.anthropic.api-key") String anthropicKey; // Nunca loguear las keys — enmascarar en logs de diagnóstico public String getMaskedKey(String provider) { return switch (provider) { case "openai" -> "sk-...****" + openAiKey.substring(openAiKey.length() - 4); case "anthropic" -> "sk-ant-...****" + anthropicKey.substring(anthropicKey.length() - 4); default -> "****"; }; } }
📝
Auditoría de Operaciones
Log estructurado de acciones sensibles con usuario, recurso y timestamp
java AuditService.java + uso en AdminResource
@ApplicationScoped public class AuditService { private static final Logger AUDIT = Logger.getLogger("agentflow.audit"); @Inject SecurityIdentity identity; public void log(String action, String resource, String resourceId) { String user = identity.isAnonymous() ? "anonymous" : identity.getPrincipal().getName(); String roles = identity.getRoles().toString(); AUDIT.infof( "AUDIT action=%s resource=%s id=%s user=%s roles=%s ts=%s", action, resource, resourceId, user, roles, Instant.now() ); } } // Uso en el recurso de administración @Path("/api/admin") @Authenticated public class AdminResource { @Inject AuditService audit; @GET @Path("/users") @RolesAllowed("ADMIN") public List<UserDto> listUsers() { audit.log("LIST", "users", "all"); return userService.findAll(); } @DELETE @Path("/users/{id}") @RolesAllowed("ADMIN") public Response deleteUser(@PathParam("id") Long id) { audit.log("DELETE", "user", String.valueOf(id)); userService.delete(id); return Response.noContent().build(); } }
☑️
Checklist de Seguridad — AgentFlow API
Verificación antes de deploy a producción
  • OIDC/JWT configurado — Keycloak realm agentflow-prod, publicKey.pem en classpath, issuer verificado
  • RBAC con @RolesAllowed — ADMIN, AGENT_MANAGER, VIEWER mapeados a claims JWT de Keycloak
  • Bean Validation en todos los POST/PUT — @Valid en CreateAgentDto y todas las entidades mutables
  • Queries parametrizadas — cero concatenación de strings en Panache/JPQL/nativas
  • Cabeceras de seguridad — X-Frame-Options, HSTS, CSP, nosniff en todas las respuestas
  • Rate limiting en /api/auth/login — máx. 5 intentos/60s por IP real (no X-Forwarded-For)
  • Secretos en Vault/env vars — ningún secreto en application.properties ni en el repositorio
  • Auditoría de operaciones ADMIN — log estructurado con usuario, rol, recurso y timestamp
  • CORS restringido — solo app.agentflow.io y admin.agentflow.io en lista blanca
  • Escaneo CVE de dependencias — mvn dependency-check:check en CI/CD pipeline