10 – Security
Version : 4.0.0 Date : 2025-12-09
1. Introduction
Le Socle V4 intègre plusieurs mécanismes de sécurité :
- Authentification Admin API (Basic Auth)
- Client JWT pour services externes (nouveauté V4)
- Filtrage des endpoints sensibles
- Gestion sécurisée des secrets
2. Authentification Admin API
2.1 Configuration
socle:
admin:
enabled: true
auth:
enabled: ${ADMIN_AUTH_ENABLED:false}
username: ${ADMIN_USERNAME:admin}
password: ${ADMIN_PASSWORD:}
2.2 AdminAuthFilter
package eu.lmvi.socle.security;
@Component
@Order(1)
public class AdminAuthFilter implements Filter {
private final SocleConfiguration config;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Skip si auth désactivée
if (!config.getAdmin().getAuth().isEnabled()) {
chain.doFilter(request, response);
return;
}
// Skip les endpoints publics
String path = httpRequest.getRequestURI();
if (isPublicEndpoint(path)) {
chain.doFilter(request, response);
return;
}
// Vérifier l'authentification pour /admin/*
if (path.startsWith("/admin")) {
String authHeader = httpRequest.getHeader("Authorization");
if (!isValidAuth(authHeader)) {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"Admin API\"");
return;
}
}
chain.doFilter(request, response);
}
private boolean isPublicEndpoint(String path) {
return path.equals("/admin/health") || path.equals("/admin/health/live");
}
private boolean isValidAuth(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Basic ")) {
return false;
}
String base64Credentials = authHeader.substring("Basic ".length());
String credentials = new String(Base64.getDecoder().decode(base64Credentials));
String[] parts = credentials.split(":", 2);
if (parts.length != 2) {
return false;
}
return parts[0].equals(config.getAdmin().getAuth().getUsername())
&& parts[1].equals(config.getAdmin().getAuth().getPassword());
}
}
2.3 Utilisation
# Sans auth
curl http://localhost:8080/admin/health
# Avec auth
curl -u admin:secret http://localhost:8080/admin/workers
3. JWT Auth Client (Nouveauté V4)
3.1 Principe
Le SocleAuthClient permet aux applications Socle de s’authentifier auprès de services centraux (LogHub, Registry, etc.).
Application Socle ──────► Auth Server ──────► Services sécurisés
│ │ │
│ 1. Login (API Key) │ │
│─────────────────────────►│ │
│ │ │
│ 2. JWT tokens │ │
│◄─────────────────────────│ │
│ │ │
│ 3. Requêtes avec Bearer │ │
│──────────────────────────────────────────────────►
3.2 Configuration
socle:
auth:
enabled: ${AUTH_ENABLED:false}
server-url: ${AUTH_SERVER_URL:https://auth.mycompany.com}
source-name: ${SOURCE_NAME:${socle.app_name}}
api-key: ${API_KEY:}
access-token-buffer-seconds: 60
3.3 Utilisation
@Service
public class SecuredApiClient {
@Autowired(required = false)
private SocleAuthClient authClient;
public void callSecuredApi() {
if (authClient == null || !authClient.isAuthenticated()) {
throw new SecurityException("Authentication required");
}
String token = authClient.getValidAccessToken();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.mycompany.com/data"))
.header("Authorization", "Bearer " + token)
.build();
// ...
}
}
Voir 23-AUTH-CLIENT pour la documentation complète.
4. Gestion des secrets
4.1 Variables d’environnement
# JAMAIS dans le code ou les fichiers committes
export ADMIN_PASSWORD="super-secret-password"
export API_KEY="my-api-key"
export REDIS_PASSWORD="redis-password"
export TECHDB_PASSWORD="techdb-password"
4.2 Docker Secrets
# docker-compose.yml
services:
app:
image: socle-v4:latest
secrets:
- admin_password
- api_key
environment:
- ADMIN_PASSWORD_FILE=/run/secrets/admin_password
- API_KEY_FILE=/run/secrets/api_key
secrets:
admin_password:
file: ./secrets/admin_password.txt
api_key:
file: ./secrets/api_key.txt
4.3 Kubernetes Secrets
apiVersion: v1
kind: Secret
metadata:
name: socle-secrets
type: Opaque
stringData:
ADMIN_PASSWORD: "super-secret"
API_KEY: "my-api-key"
---
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
envFrom:
- secretRef:
name: socle-secrets
4.4 Configuration sécurisée
@Configuration
public class SecretsConfiguration {
@Value("${ADMIN_PASSWORD:#{null}}")
private String adminPassword;
@Value("${ADMIN_PASSWORD_FILE:#{null}}")
private String adminPasswordFile;
@PostConstruct
public void loadSecrets() {
// Charger depuis fichier si spécifié
if (adminPasswordFile != null) {
try {
adminPassword = Files.readString(Path.of(adminPasswordFile)).trim();
} catch (IOException e) {
throw new RuntimeException("Failed to load secret from file", e);
}
}
}
public String getAdminPassword() {
return adminPassword;
}
}
5. CORS Configuration
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {
@Value("${socle.security.cors.allowed-origins:*}")
private String allowedOrigins;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(allowedOrigins.split(","))
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count")
.allowCredentials(true)
.maxAge(3600);
}
}
6. Rate Limiting
6.1 Implémentation simple
@Component
public class RateLimitFilter implements Filter {
private final KvBus kvBus;
private final int maxRequests = 100;
private final Duration window = Duration.ofMinutes(1);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientId = getClientId(httpRequest);
String key = "ratelimit:" + clientId + ":" + Instant.now().truncatedTo(ChronoUnit.MINUTES);
long count = kvBus.increment(key);
if (count == 1) {
kvBus.setTtl(key, window);
}
if (count > maxRequests) {
httpResponse.setStatus(429);
httpResponse.setHeader("Retry-After", "60");
httpResponse.getWriter().write("Rate limit exceeded");
return;
}
httpResponse.setHeader("X-RateLimit-Limit", String.valueOf(maxRequests));
httpResponse.setHeader("X-RateLimit-Remaining", String.valueOf(maxRequests - count));
chain.doFilter(request, response);
}
private String getClientId(HttpServletRequest request) {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null) {
return apiKey;
}
return request.getRemoteAddr();
}
}
7. Input Validation
7.1 Validation des DTOs
public record CreateOrderRequest(
@NotNull @Size(min = 1, max = 100)
String customerId,
@NotEmpty
List<@Valid OrderItem> items,
@Email
String notificationEmail
) {}
public record OrderItem(
@NotNull @Size(min = 1, max = 50)
String productId,
@Min(1) @Max(1000)
int quantity
) {}
7.2 Controller avec validation
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<Order> createOrder(@Valid @RequestBody CreateOrderRequest request) {
// request est validé automatiquement
return ResponseEntity.ok(orderService.create(request));
}
}
7.3 Sanitization
public class InputSanitizer {
private static final Pattern SAFE_STRING = Pattern.compile("^[a-zA-Z0-9-_]+$");
public static String sanitizeId(String input) {
if (input == null) return null;
if (!SAFE_STRING.matcher(input).matches()) {
throw new IllegalArgumentException("Invalid ID format");
}
return input;
}
public static String sanitizeForLog(String input) {
if (input == null) return null;
return input.replaceAll("[\n\r\t]", "_");
}
}
8. Logging sécurisé
8.1 Ne pas logger les secrets
// MAUVAIS
log.info("Connecting with password: {}", password);
log.info("API Key: {}", apiKey);
log.info("Request: {}", requestWithSensitiveData);
// BON
log.info("Connecting to database");
log.info("Using API Key: {}...", apiKey.substring(0, 4));
log.info("Request received for user: {}", sanitizeForLog(userId));
8.2 Pattern pour masquer les données sensibles
public class SecureLogger {
private static final Set<String> SENSITIVE_FIELDS = Set.of(
"password", "apiKey", "token", "secret", "credential"
);
public static String maskSensitiveData(Map<String, Object> data) {
Map<String, Object> masked = new HashMap<>();
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (SENSITIVE_FIELDS.contains(entry.getKey().toLowerCase())) {
masked.put(entry.getKey(), "***MASKED***");
} else {
masked.put(entry.getKey(), entry.getValue());
}
}
return masked.toString();
}
}
9. Headers de sécurité
@Component
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Prevent clickjacking
httpResponse.setHeader("X-Frame-Options", "DENY");
// XSS protection
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
// HSTS (si HTTPS)
httpResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
// Content Security Policy
httpResponse.setHeader("Content-Security-Policy", "default-src 'self'");
chain.doFilter(request, response);
}
}
10. Audit logging
@Aspect
@Component
public class AuditAspect {
private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");
@Around("@annotation(Audited)")
public Object audit(ProceedingJoinPoint joinPoint) throws Throwable {
String method = joinPoint.getSignature().getName();
String user = getCurrentUser();
Instant start = Instant.now();
try {
Object result = joinPoint.proceed();
auditLog.info("SUCCESS | user={} | method={} | duration={}ms",
user, method, Duration.between(start, Instant.now()).toMillis());
return result;
} catch (Exception e) {
auditLog.warn("FAILURE | user={} | method={} | error={}",
user, method, e.getMessage());
throw e;
}
}
private String getCurrentUser() {
// Récupérer l'utilisateur du contexte
return "system";
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audited {}
11. Checklist de sécurité
Configuration
- [ ]
ADMIN_AUTH_ENABLED=trueen production - [ ] Mots de passe forts et uniques
- [ ] Secrets via variables d’environnement ou secret manager
- [ ] HTTPS activé
- [ ] CORS configuré correctement
Code
- [ ] Validation de tous les inputs
- [ ] Pas de secrets dans les logs
- [ ] Headers de sécurité activés
- [ ] Rate limiting en place
- [ ] Audit logging activé
Infrastructure
- [ ] Firewall configuré
- [ ] Ports non nécessaires fermés
- [ ] H2 Console désactivée en production
- [ ] Accès admin restreint
12. Références
- 23-AUTH-CLIENT – Client JWT
- 13-TLS-HTTPS – Configuration TLS
- 14-ADMIN-API – API Admin
- OWASP Top 10

Laisser un commentaire