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 / OIDC —
ReactiveJwtDecoder+ JWT issuer·audience - API Key = 외부 한정 (내부는 mTLS·JWT)
- 회전·만료 어려움
- gRPC-Web (브라우저) — CORS 설정
- Service Mesh (Istio·Linkerd) = 자동 mTLS + 라우팅
- 운영 — TLS·mTLS·JWT·@PreAuthorize·Rate Limit·Audit·짧은 토큰
Context.Key로 인터셉터 ↔ 서비스 정보 전파
시리즈 다른 편
- 1편 — 기본 개념·HTTP/2·4 RPC 모드
- 2편 — Protocol Buffers
- 3편 — Unary RPC
- 4편 — Server Streaming
- 5편 — Client Streaming
- 6편 — Bidirectional Streaming
- 7편 — Interceptors
- 8편 — Error Handling
- 9편 — Security (현재 글)
- 10편 — 고급 (Reflection·Health·LB·gRPC-Web)
공식 문서: gRPC Authentication 에서 더 깊이.
다음 글(10편, 마지막)에서는 고급 주제 — Reflection, Health Check, Compression, Deadline·Channel 관리, gRPC-Web, Load Balancing까지 시리즈 마무리.