gRPC + Spring Boot — Security·TLS·인증

2026-05-03확률과 통계 마스터 노트

gRPC + Spring Boot 마스터 노트 시리즈 9편. gRPC 보안의 3 축(인증·인가·암호화), TLS 1-way·mTLS 차이, JWT 토큰 인증 인터셉터, Spring Security 통합, ALTS(Google 내부 표준), 라우트별 권한 제어, 서비스 간 mTLS 패턴까지.

이 글은 gRPC + Spring Boot 마스터 노트 시리즈의 아홉 번째 편입니다. 1~8편이 기능이었다면, 이번엔 그것을 안전하게 — 보안.

gRPC 보안 = HTTP/2 위 TLS + 인증 인터셉터. 단순하지만 운영 디테일이 많음. 마이크로서비스 사이 mTLS가 표준.

처음 gRPC 보안이 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, TLS·mTLS·ALTS 3 옵션이 헷갈립니다. 둘째, 인증 인터셉터 vs Spring Security 어느 쪽 쓸지 막연합니다.

해결법은 한 가지예요. "TLS = 암호화 / mTLS = 양쪽 인증서 / 인증 = 인터셉터에서" 한 줄. 모든 운영 = TLS 필수. 마이크로서비스 = mTLS 권장.

보안 3 축

1. Authentication (인증) — 너 누구냐
2. Authorization (인가)  — 뭘 할 수 있냐
3. Encryption (암호화)   — TLS·mTLS

TLS — Transport Layer Security

서버 — TLS 활성화

grpc:
  server:
    port: 9090
    security:
      enabled: true
      certificate-chain: classpath:server.crt
      private-key: classpath:server.key

또는:

@Bean
public GrpcServerConfigurer grpcServerConfigurer() {
    return serverBuilder -> {
        try {
            ((NettyServerBuilder) serverBuilder)
                .sslContext(GrpcSslContexts.forServer(
                    new File("server.crt"),
                    new File("server.key")
                ).build());
        } catch (SSLException e) {
            throw new RuntimeException(e);
        }
    };
}

클라이언트 — TLS

grpc:
  client:
    user-service:
      address: static://user-service:9090
      negotiation-type: tls
      security:
        trust-cert-collection: classpath:ca.crt

여기서 정말 중요한 시험 함정 — 운영 = TLS 필수. plaintext는 개발만. 메타데이터·페이로드 평문 노출.

mTLS — Mutual TLS

서버·클라이언트 모두 인증서로 인증.

서버

grpc:
  server:
    security:
      enabled: true
      certificate-chain: classpath:server.crt
      private-key: classpath:server.key
      trust-cert-collection: classpath:ca.crt
      client-auth: REQUIRE             # 강제

클라이언트

grpc:
  client:
    user-service:
      negotiation-type: tls
      security:
        certificate-chain: classpath:client.crt
        private-key: classpath:client.key
        trust-cert-collection: classpath:ca.crt

여기서 정말 중요한 시험 함정 — 마이크로서비스 사이 mTLS가 표준. API 키·토큰 대안. Istio·Linkerd 같은 Service Mesh 자동 적용.

JWT 인증

인터셉터

public class JwtAuthInterceptor implements ServerInterceptor {
    
    private static final Metadata.Key<String> AUTH = 
        Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
    
    @Autowired
    private JwtVerifier verifier;
    
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
        ServerCall<ReqT, RespT> call,
        Metadata headers,
        ServerCallHandler<ReqT, RespT> next
    ) {
        String authHeader = headers.get(AUTH);
        
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            call.close(Status.UNAUTHENTICATED.withDescription("Missing token"), new Metadata());
            return new ServerCall.Listener<ReqT>() {};
        }
        
        try {
            String token = authHeader.substring(7);
            Claims claims = verifier.verify(token);
            
            Context ctx = Context.current()
                .withValue(USER_KEY, claims.getSubject())
                .withValue(ROLES_KEY, claims.get("roles"));
            
            return Contexts.interceptCall(ctx, call, headers, next);
        } catch (Exception e) {
            call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), new Metadata());
            return new ServerCall.Listener<ReqT>() {};
        }
    }
}

클라이언트 — 토큰 첨부

public class JwtClientInterceptor implements ClientInterceptor {
    
    private final TokenProvider tokenProvider;
    
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
        MethodDescriptor<ReqT, RespT> method,
        CallOptions options,
        Channel next
    ) {
        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
            next.newCall(method, options)
        ) {
            @Override
            public void start(Listener<RespT> listener, Metadata headers) {
                headers.put(
                    Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER),
                    "Bearer " + tokenProvider.getToken()
                );
                super.start(listener, headers);
            }
        };
    }
}

Spring Security 통합

의존성

implementation 'net.devh:grpc-server-spring-boot-starter:3.x'
implementation 'org.springframework.boot:spring-boot-starter-security'

설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public GrpcAuthenticationReader authenticationReader() {
        List<GrpcAuthenticationReader> readers = new ArrayList<>();
        readers.add(new BearerAuthenticationReader(BearerTokenAuthenticationToken::new));
        return new CompositeGrpcAuthenticationReader(readers);
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
        ReactiveJwtDecoder jwtDecoder
    ) {
        // JWT 검증 매니저
    }
    
    @Bean
    public AccessDecisionManager accessDecisionManager() {
        return new UnanimousBased(List.of(new AccessPredicateVoter()));
    }
}

라우트별 권한

@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
    
    @Override
    @PreAuthorize("hasRole('USER')")
    public void getUser(UserRequest req, StreamObserver<User> observer) { ... }
    
    @Override
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(DeleteRequest req, StreamObserver<Empty> observer) { ... }
    
    @Override
    @PreAuthorize("authentication.name == #req.userId")
    public void updateProfile(UpdateRequest req, StreamObserver<Empty> observer) { ... }
}

@EnableMethodSecurity 활성화 필요.

ALTS — Application Layer Transport Security

Google이 만든 GCP 내부 표준. mTLS 대안.

ManagedChannel channel = AltsChannelBuilder
    .forTarget("server:9090")
    .build();

GCP 환경 (GKE 등)에서 자동 동작. 인증서 관리 X.

여기서 시험 함정이 하나 있어요. ALTS는 GCP 환경 전용. 일반 환경 = TLS·mTLS. 다중 클라우드 = TLS·mTLS만.

OAuth2 / OIDC

@Configuration
public class OAuth2Config {
    
    @Bean
    public ServerInterceptor oauth2Interceptor(ReactiveJwtDecoder decoder) {
        return new ServerInterceptor() {
            @Override
            public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
                ServerCall<ReqT, RespT> call,
                Metadata headers,
                ServerCallHandler<ReqT, RespT> next
            ) {
                String token = extractBearerToken(headers);
                
                Jwt jwt = decoder.decode(token).block();
                if (jwt == null) {
                    call.close(Status.UNAUTHENTICATED, new Metadata());
                    return new ServerCall.Listener<ReqT>() {};
                }
                
                Context ctx = Context.current().withValue(JWT_KEY, jwt);
                return Contexts.interceptCall(ctx, call, headers, next);
            }
        };
    }
}

OAuth2 Resource Server 패턴. JWT issuer·audience 검증.

API Key

간단한 인증 — 헤더에 API Key:

public class ApiKeyInterceptor implements ServerInterceptor {
    
    private static final Metadata.Key<String> API_KEY = 
        Metadata.Key.of("x-api-key", Metadata.ASCII_STRING_MARSHALLER);
    
    @Autowired
    private ApiKeyService apiKeyService;
    
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
        ServerCall<ReqT, RespT> call,
        Metadata headers,
        ServerCallHandler<ReqT, RespT> next
    ) {
        String apiKey = headers.get(API_KEY);
        
        if (apiKey == null || !apiKeyService.verify(apiKey)) {
            call.close(Status.UNAUTHENTICATED.withDescription("Invalid API key"), new Metadata());
            return new ServerCall.Listener<ReqT>() {};
        }
        
        return next.startCall(call, headers);
    }
}

여기서 시험 함정이 하나 있어요. API Key는 외부 클라이언트용. 내부 마이크로서비스 = mTLS 또는 JWT 권장. API Key는 회전·만료 어려움.

CORS — gRPC-Web

브라우저에서 gRPC = gRPC-Web. CORS 설정 필요.

@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    config.addAllowedOrigin("https://example.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    return new CorsFilter(...);
}

보안 체크리스트

✓ TLS 활성화 (운영)
✓ mTLS (마이크로서비스 사이)
✓ Service Mesh (Istio·Linkerd 자동 mTLS)
✓ JWT 또는 OAuth2 (외부 인증)
✓ Spring Security 통합
✓ @PreAuthorize (메서드별 권한)
✓ API Key (외부 한정)
✓ Rate Limit (인터셉터)
✓ Audit Log
✓ 토큰 만료 짧게 (수 분~수 시간)
✓ Refresh Token 별도
✓ 로그에 토큰 출력 X
✓ Validation (입력)

인증 정보 컨텍스트 전파

public static final Context.Key<String> USER_ID = Context.key("user-id");

@Override
public void getProfile(Empty req, StreamObserver<Profile> observer) {
    String userId = USER_ID.get();
    
    // 비즈니스 로직
}

Context.Key로 인터셉터 → 서비스 정보 전달.

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 9편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • 보안 3 축 — 인증·인가·암호화
  • TLS = 단방향 암호화·서버 인증서만
  • mTLS = 양방향, 클라이언트도 인증서
  • 운영 = TLS 필수
  • 마이크로서비스 = mTLS 권장
  • yaml grpc.server.security.enabled: true + cert·key
  • client-auth: REQUIRE = mTLS 강제
  • JWT 인증 = 인터셉터에서 Authorization: Bearer ... 검증
  • 검증 후 Context.Key로 사용자 정보 저장
  • 클라이언트 — ForwardingClientCall + headers.put
  • Spring Security 통합@PreAuthorize·@EnableMethodSecurity
  • BearerAuthenticationReader 등록
  • ALTS = GCP 전용 (mTLS 대안)
  • 다중 클라우드 = TLS·mTLS만
  • OAuth2 / OIDCReactiveJwtDecoder + JWT issuer·audience
  • API Key = 외부 한정 (내부는 mTLS·JWT)
  • 회전·만료 어려움
  • gRPC-Web (브라우저) — CORS 설정
  • Service Mesh (Istio·Linkerd) = 자동 mTLS + 라우팅
  • 운영 — TLS·mTLS·JWT·@PreAuthorize·Rate Limit·Audit·짧은 토큰
  • Context.Key로 인터셉터 ↔ 서비스 정보 전파

시리즈 다른 편

공식 문서: gRPC Authentication 에서 더 깊이.

다음 글(10편, 마지막)에서는 고급 주제 — Reflection, Health Check, Compression, Deadline·Channel 관리, gRPC-Web, Load Balancing까지 시리즈 마무리.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!