Socle V004 – Sécurité

Socle V004 - Sécurité

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=true en 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

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *