Опирается на правила: AUTH-16AUTH-18 из Auth Patterns Style Guide → раздел 7. PII и секреты.

Важно знать

  • PII не в логах (даже DEBUG), не в Exception.getMessage, не в ProblemDetails.detail, не в Kafka-events широкого scope.
  • Передавать только id, payload подгружается через специальный сервис по запросу.
  • Секреты никогда в git — только через application-${profile}.yml в Vault / SealedSecrets / env vars.
  • RestControllerAdvice не выводит cause.getMessage() в detail — только заранее заданное сообщение по коду.
  • PII leak в одном слое — пробивает всю безопасность системы.
  • @Slf4j log.info("User: {}", user) — потенциальная утечка если в toString() есть PII.

PII (Personally Identifiable Information) — это данные, которые регулирующие органы и compliance считают защищаемыми: email, телефон, ФИО, паспорт, адрес, IP, биометрия. Утечка через лог, через response, через Kafka — это инцидент compliance + штраф + потеря доверия. UCP формулирует правила «PII никогда не покидает domain layer».

PII не в логах

AUTH-16: запрет на все уровни логирования.

// КАТАСТРОФА
log.info("User registered: email={} phone={}", user.email(), user.phone());

// ХОРОШО — только internal id
log.info("User registered: userId={}", user.id());

// ЕСЛИ нужен PII для диагностики — masked
log.info("Email verification sent: userId={} emailMask={}",
    user.id(), maskEmail(user.email()));   // u***@example.com

Маскирование:

public final class PiiMasking {
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) return "***";
        var parts = email.split("@");
        return parts[0].charAt(0) + "***@" + parts[1];
    }

    public static String maskPhone(String phone) {
        if (phone == null || phone.length() < 4) return "***";
        return "***" + phone.substring(phone.length() - 4);
    }
}

Дополнительно — toString() агрегата не возвращает PII:

public record Customer(Long id, String email, String phone, String fullName) {
    @Override
    public String toString() {
        return "Customer[id=" + id + "]";
    }
}

Подробнее — Logging → PII-гигиена.

PII не в Exception.getMessage()

AUTH-16 + AUTH-18: exception message может попасть в logs И в response.

// КАТАСТРОФА
throw new InvalidEmailException("Email " + email + " is invalid format");

Что происходит:

  • log.error(...) пишет message в лог → PII leak.
  • RestControllerAdvice mapping → ProblemDetails.detail = "Email user@example.com is invalid format" → клиент (включая attacker, который пытается enumerate emails) видит подтверждение.

Корректно:

public class InvalidEmailException extends DomainException {
    public InvalidEmailException() {
        super("INVALID_EMAIL_FORMAT", "Provided email is in invalid format");
    }
}

В message и detail — общее сообщение без значения. Если нужно для диагностики — лог с masked email, не exception.

RestControllerAdvice без cause.getMessage()

AUTH-18: handler-mapping явный, не «прокинуть exception дальше».

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(OrderDomainException.class)
    public ProblemDetail handleOrderDomain(OrderDomainException ex) {
        var problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        problem.setType(URI.create("urn:order:domain"));
        problem.setTitle("Order operation failed");
        problem.setDetail(switch (ex.errorCode()) {
            case "ORDER_NOT_FOUND" -> "Order with given id not found";
            case "ORDER_NOT_CANCELLABLE" -> "Order in current status cannot be cancelled";
            default -> "Order operation failed";
        });
        problem.setProperty("errorCode", ex.errorCode());
        return problem;
    }

    @ExceptionHandler(Exception.class)
    public ProblemDetail handleGeneric(Exception ex) {
        var problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
        problem.setTitle("Internal server error");
        problem.setDetail("An unexpected error occurred. Reference: " + MDC.get("requestId"));
        return problem;
    }
}

Что не делаем:

// ПЛОХО
problem.setDetail(ex.getMessage());                    // может содержать PII
problem.setDetail(ex.getCause().getMessage());         // тем более внутренние details
problem.setProperty("stackTrace", ex.getStackTrace()); // утечка структуры

Подробнее — Error handling → RFC 9457.

PII не в Kafka широкого scope

AUTH-16: Kafka — broadcast canal.

// ПЛОХО — все consumers видят PII
public record OrderConfirmedEvent(
    Long orderId,
    String customerEmail,    // ← leak
    String customerPhone     // ← leak
) {}

// ХОРОШО — id, PII подгружается через customer-service по необходимости
public record OrderConfirmedEvent(
    Long orderId,
    Long customerId,
    Money totalAmount
) {}

Notification-service, которому нужен email — делает GET /customers/{id}/email к customer-service. Это даёт точечный access + audit log на customer-service-side.

Подробнее — Kafka → event design и Kafka → security.

Секреты не в git

AUTH-17: правило для всех уровней.

# КАТАСТРОФА — в git
spring:
  datasource:
    password: super-secret-password-prod

# ХОРОШО — env
spring:
  datasource:
    password: ${DB_PASSWORD}

Откуда брать значения env vars:

  1. Vault (HashiCorp) — kubectl через vault-secrets-operator или Spring Cloud Vault.
  2. SealedSecrets (Kubernetes) — шифрованный secret в git, расшифровывается оператором в кластере.
  3. Cloud Secret Manager (AWS SM, GCP SM) — IAM-роль pod-а получает secret напрямую.
  4. Kubernetes SecretimagePullPolicy-style рестрикциями) — минимальный baseline.

.gitignore со списком sensitive файлов:

application-prod.yml
application-secrets.yml
*.pem
*.key
.env

Pre-commit hook через git-secrets или trufflehog — отдельная защита от случайного commit.

При обнаружении secret в git history — rotate секрет (даже если коммит удалён, он может быть в forks, CI cache, attacker уже клонировал).

Что запрещено

АнтипаттернПравилоЧто взамен
PII в логах (email, phone)AUTH-16masked или только id
PII в Exception.getMessage()AUTH-16 + AUTH-18error code, общее сообщение
PII в ProblemDetails.detailAUTH-18mapping по error code
cause.getMessage() в detailAUTH-18заранее заданное сообщение
PII в Kafka-events широкого scopeAUTH-16только id + lazy fetch
password: super-secret в application.ymlAUTH-17env ${DB_PASSWORD}
Secrets в git historyAUTH-17rotate immediately
stackTrace в responseAUTH-18только traceId для cross-ref
@Slf4j log.info("User: {}", user) с PII в toStringAUTH-16toString без PII

Куда дальше