Опирается на правила: AUTH-13AUTH-14 из Auth Patterns Style Guide → раздел 5. Service-to-service.

Важно знать

  • Два способа межсервисной аутентификации: mTLS (рекомендуется) или Client Credentials Flow.
  • mTLS — двусторонний TLS на K8s Service Mesh (Istio, Linkerd). Identity = CN client-сертификата.
  • Client Credentials Flowgrant_type=client_credentials, сервис получает access_token от IdP с scope=service:operation.
  • Анонимный inter-service трафик — критическое нарушение. Любой RestClient без mTLS или Bearer — на ревью.
  • mTLS даёт автоматическую identity-привязку — не нужно вручную проставлять headers.
  • Один кросс-сервисный токен с broad scope — антипаттерн (нет blast-radius containment).
  • Hard-coded shared secret в коде — никогда. Только env / Vault.

Service-to-service вызовы внутри кластера часто кажутся «безопасными по определению» — VPC isolated, internal network. Это иллюзия: insider attack, compromised pod, lateral movement — реальные сценарии. UCP формулирует правило «zero trust»: каждый межсервисный вызов аутентифицирован.

Способ 1: mTLS (рекомендуется)

AUTH-13: двусторонний TLS через Service Mesh.

order-service pod → istio-proxy sidecar → mTLS encrypted → istio-proxy → payment-service pod
                    (cert: order-service-prod)              (verifies cert)

Istio автоматически:

  • Раздаёт каждому pod-у уникальный client-сертификат (через SPIFFE identity).
  • Шифрует все internal-вызовы TLS 1.2+.
  • Verify-ит client cert на receiver-side.

В Spring приложении ничего custom не нужно — sidecar обрабатывает mTLS до Spring. Receiver получает identity через header (X-Forwarded-Client-Cert или Istio-specific) или через mTLS-аутентификацию на стороне Spring Security, если sidecar её прокидывает.

@Configuration
@ConditionalOnProperty(name = "security.mtls.enabled", havingValue = "true")
public class MtlsSecurityConfig {

    @Bean
    SecurityFilterChain internalApi(HttpSecurity http) throws Exception {
        return http
            .securityMatcher("/internal/**")
            .x509(x509 -> x509
                .subjectPrincipalRegex("CN=(.*?)(?:,|$)")
                .userDetailsService(serviceUserDetails()))
            .authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
            .build();
    }

    @Bean
    UserDetailsService serviceUserDetails() {
        return username -> User.builder()
            .username(username)
            .password("")
            .authorities("ROLE_system")
            .build();
    }
}

Преимущества mTLS:

  • Identity встроена в transport — нечего «забыть» добавить в headers.
  • Rotation автоматическая — Istio обновляет сертификаты каждые 24 часа.
  • Cross-language — работает одинаково для Java, Go, Python сервисов.

Недостатки:

  • Требует Service Mesh инфры (Istio/Linkerd) — это серьёзная инфра-нагрузка.
  • В dev/test без Service Mesh — нужны альтернативы.

Способ 2: Client Credentials Flow

AUTH-13: OAuth2 standard.

order-service → IdP: POST /oauth/token
                grant_type=client_credentials
                client_id=order-service-prod
                client_secret=$ORDER_SERVICE_CLIENT_SECRET
                scope=payment:charge

← IdP: { "access_token": "...", "expires_in": 3600 }

order-service → payment-service:
                POST /charge
                Authorization: Bearer <token>

payment-service: oauth2ResourceServer.jwt() валидирует токен,
                  видит `scope=payment:charge`, разрешает.
spring:
  security:
    oauth2:
      client:
        registration:
          payment-service:
            client-id: order-service-prod
            client-secret: ${ORDER_SERVICE_CLIENT_SECRET}
            authorization-grant-type: client_credentials
            scope: payment:charge
        provider:
          payment-service:
            token-uri: ${IDP_TOKEN_URI}
@Configuration
@RequiredArgsConstructor
public class PaymentClientConfig {

    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository registrations,
        OAuth2AuthorizedClientService clients
    ) {
        var provider = OAuth2AuthorizedClientProviderBuilder.builder()
            .clientCredentials()
            .build();
        var manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrations, clients);
        manager.setAuthorizedClientProvider(provider);
        return manager;
    }

    @Bean
    RestClient paymentRestClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        var interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
        interceptor.setClientRegistrationIdResolver(req -> "payment-service");
        return RestClient.builder()
            .baseUrl("https://payment-service.internal")
            .requestInterceptor(interceptor)
            .build();
    }
}

Spring Security автоматически:

  • Запрашивает token у IdP при первом вызове.
  • Кеширует token до exp - 30s.
  • Добавляет Authorization: Bearer <token> к каждому requests.
  • Refresh-ит при expiry.

В UCP-сервисах именованные scope per operation (payment:charge, payment:refund, inventory:reserve), не general service. Это даёт fine-grained ACL: order-service может charge, но не может refund.

Запрет анонимного трафика

AUTH-14: RestTemplate.exchange(...) без auth — критическое нарушение.

// КАТАСТРОФА
@Component
public class PaymentClient {
    private final RestTemplate restTemplate;

    public Receipt charge(Long orderId, Money amount) {
        return restTemplate.postForObject(
            "http://payment-service/charge",
            new ChargeRequest(orderId, amount),
            Receipt.class
        );
    }
}

Любой компонент в сети может вызывать payment-service. Один скомпрометированный pod (например, через image vulnerability) → доступ ко всему cluster.

Корректно: либо mTLS через sidecar (нет custom-кода для auth), либо OAuth2 Client (RestClient с OAuth2ClientHttpRequestInterceptor).

ArchUnit-правило для проверки:

@ArchTest
public static final ArchRule outAdapterUsesAuthorizedClient =
    classes()
        .that().resideInAPackage("..adapter.out..")
        .and().haveSimpleNameEndingWith("Client")
        .should().dependOnClassesThat().haveSimpleName("OAuth2AuthorizedClientManager")
        .orShould().dependOnClassesThat().haveSimpleName("OAuth2ClientHttpRequestInterceptor");

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

АнтипаттернПравилоЧто взамен
RestTemplate.exchange без AuthorizationAUTH-14mTLS или OAuth2 Client
Анонимный inter-service трафикAUTH-14zero trust
Hard-coded shared secret для S2SAUTH-17env / Vault
Один токен на все S2S операцииAUTH-13scope per operation
client_secret в application.ymlAUTH-17env через ${ORDER_SERVICE_CLIENT_SECRET}
Manual token cachingAUTH-13OAuth2AuthorizedClientManager делает сам
mTLS только для public-facingAUTH-13zero trust в internal тоже
RestClient без interceptor — auth добавлен в HandlerAUTH-14interceptor централизованно

Куда дальше

  • Auth → раздел 5. Service-to-service — нормативные формулировки.
  • JWT validation — payment-service валидирует токен от order-service.
  • PII и секреты — client_secret через Vault.
  • Resilience → integration — RestClient per-external-system.
  • Kafka → security — TLS + ACL для Kafka cross-service.
  • Где какая проверка — system role для S2S.