보안(TLS/mTLS)과 인증
gRPC 보안의 두 계층
gRPC 보안은 크게 두 질문에 답해야 한다.
- "이 통신은 도청·위변조로부터 안전한가?" — 전송 계층(Transport Layer)의 문제
- "이 호출자는 누구이고, 무엇을 허용할 것인가?" — 인증·인가(Authn/Authz)의 문제
gRPC는 SSL/TLS를 전송 보안의 기본 메커니즘으로 정의한다. 그 위에 토큰, 인증서 기반 ID, 서비스 메시 제공 ID 같은 애플리케이션 레벨 인증을 조합한다. 두 계층은 독립적이지만 함께 설계해야 한다. 예를 들어 TLS 없이 JWT를 metadata로 보내면 토큰이 평문으로 노출된다.
per-RPC token API Key
metadata ALTS
(GCP 전용) 인터셉터로 검증
(단방향) mTLS
(상호 인증) Channel Credentials
TLS: 단방향 전송 보안
TLS는 서버가 인증서를 제시하고, 클라이언트가 이를 검증하는 방식으로 동작한다. 클라이언트는 "내가 연결한 서버가 신뢰할 수 있는 서버인가"를 확인하고, 이후 모든 통신은 암호화된다.
TLS 핸드셰이크 흐름
Go에서 서버는 인증서와 개인키 파일로 TLS 채널 크레덴셜을 만든다.
// 서버
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
server := grpc.NewServer(grpc.Creds(creds))
// 클라이언트 (CA 인증서로 서버 인증서 검증)
creds, err := credentials.NewClientTLSFromFile("ca.crt", "")
conn, err := grpc.NewClient("myservice:50051", grpc.WithTransportCredentials(creds))개발 환경이라면 insecure.NewCredentials()로 TLS를 끄기도 하지만, 운영 환경에서는 반드시 TLS를 활성화해야 한다. 평문 gRPC는 HTTP/2 헤더(metadata의 authorization 토큰 등)가 그대로 노출된다.
mTLS: 상호 인증
일반 TLS는 클라이언트가 서버를 검증한다. mTLS(Mutual TLS) 는 서버도 클라이언트의 인증서를 요구하고 검증한다. 둘 다 서로의 신원을 암호학적으로 증명하므로, 서비스 간 통신(east-west traffic)에서 "호출한 쪽이 신뢰받는 서비스인가"를 전송 계층에서 확인할 수 있다.
| 구분 | TLS (단방향) | mTLS (상호) |
|---|---|---|
| 서버 인증서 | 필요 | 필요 |
| 클라이언트 인증서 | 불필요 | 필요 |
| 주요 용도 | 클라이언트-서버 공개 API | 서비스 간 내부 통신 |
| 신원 보증 | 서버 신원만 | 서버 + 클라이언트 신원 모두 |
// 서버: 클라이언트 인증서 요구
cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
caCert, _ := os.ReadFile("ca.crt")
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert, // mTLS 핵심
ClientCAs: caPool,
}
creds := credentials.NewTLS(tlsConfig)
server := grpc.NewServer(grpc.Creds(creds))
// 클라이언트: 자신의 인증서 제시
clientCert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caPool,
}
conn, _ := grpc.NewClient("myservice:50051",
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))mTLS의 운영 부담은 인증서 발급·갱신·배포다. Kubernetes 환경이라면 cert-manager가 Let's Encrypt 또는 내부 CA와 연동해 인증서를 자동으로 발급하고 Secret에 저장한다. Istio 같은 서비스 메시는 사이드카가 인증서 갱신을 투명하게 처리하므로 애플리케이션 코드를 건드리지 않고 전체 메시 내 mTLS를 강제할 수 있다.
채널 크레덴셜 vs 콜 크레덴셜
gRPC 인증 API는 두 종류의 크레덴셜을 구분한다.
| 종류 | 범위 | 대표 예 |
|---|---|---|
| 채널 크레덴셜 (Channel Credentials) | 채널 전체, 연결 수립 시 적용 | TLS, mTLS, ALTS |
| 콜 크레덴셜 (Call Credentials / Per-RPC Credentials) | RPC 하나, metadata에 첨부 | JWT, OAuth2 Bearer token, API Key |
채널 크레덴셜은 한 번 설정하면 해당 채널의 모든 RPC에 적용된다. 콜 크레덴셜은 매 RPC마다 다른 토큰을 첨부하거나, 사용자 세션에 따라 다른 권한을 표현할 때 쓴다.
두 크레덴셜은 복합 크레덴셜(Composite Credentials) 로 조합할 수 있다. 대부분의 실전 시나리오는 TLS(채널 암호화) + JWT(호출자 신원)를 함께 사용한다.
// 채널 크레덴셜 + 콜 크레덴셜 조합 (Go)
tlsCreds, _ := credentials.NewClientTLSFromFile("ca.crt", "")
// Per-RPC 토큰 크레덴셜 구현
type tokenAuth struct{ token string }
func (t tokenAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
return map[string]string{"authorization": "Bearer " + t.token}, nil
}
func (t tokenAuth) RequireTransportSecurity() bool { return true } // TLS 없으면 거부
conn, _ := grpc.NewClient("myservice:50051",
grpc.WithTransportCredentials(tlsCreds),
grpc.WithPerRPCCredentials(tokenAuth{token: "eyJhbG..."}),
)RequireTransportSecurity() bool이 true를 반환하면, TLS가 없는 채널에서는 콜 크레덴셜이 첨부되지 않는다. 이것이 중요한 안전장치다. 평문 채널에서 JWT를 실수로 보내는 상황을 라이브러리 수준에서 차단한다.
JWT와 토큰 기반 인증 흐름
(Auth Server) → JWT 수령
authorization: Bearer <JWT> → Server
Interceptor
claims 추출 → RPC Handler
(principal 주입)
서버 측 인터셉터에서 metadata를 꺼내 JWT를 검증하는 패턴은 다음과 같다.
func authInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
values := md["authorization"]
if len(values) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
token := strings.TrimPrefix(values[0], "Bearer ")
claims, err := verifyJWT(token) // 서명 검증 + exp 확인
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
ctx = context.WithValue(ctx, principalKey, claims.Subject)
return handler(ctx, req)
}JWT 검증 시 주의해야 할 사항이 있다. 서명 알고리즘을 alg: none으로 허용하면 서명 우회가 가능하므로 허용 알고리즘을 명시적으로 화이트리스트해야 한다. 또한 exp (만료 시간)와 iss (발급자) 클레임을 반드시 검증한다. 짧은 수명의 access token과 refresh 흐름을 별도로 두는 것이 일반적이다.
ALTS: GCP 환경의 전용 전송 보안
ALTS(Application Layer Transport Security) 는 Google이 내부 인프라를 위해 설계한 상호 인증 + 암호화 프로토콜이다. mTLS와 유사하지만 GCP 인프라(Compute Engine, GKE)와 긴밀하게 통합되어 있어 인증서를 직접 관리하지 않아도 된다. VM 또는 GKE 노드에 부여된 서비스 계정 ID를 자동으로 활용한다.
GCP 외부에서는 동작하지 않으므로, 퍼블릭 클라우드가 아닌 환경이나 멀티클라우드에서는 일반 TLS/mTLS를 써야 한다.
// Go에서 ALTS 사용 (GCP 환경만 유효)
import "google.golang.org/grpc/credentials/alts"
altsTC := alts.NewClientCreds(alts.DefaultClientOptions())
conn, _ := grpc.NewClient("myservice:50051",
grpc.WithTransportCredentials(altsTC))서비스 메시와 mTLS 자동화
Kubernetes 환경에서 Istio를 쓴다면, 개별 gRPC 서비스 코드에 TLS 설정이 없어도 사이드카 프록시(Envoy) 가 파드 사이의 트래픽을 자동으로 mTLS로 암호화한다.
(gRPC app) → Envoy
sidecar A ⇄ mTLS ⇄ Envoy
sidecar B → Service B
(gRPC app)
Istio의 PeerAuthentication 정책을 STRICT로 설정하면 메시 내 모든 통신이 mTLS를 강제한다. 애플리케이션 코드는 여전히 insecure.NewCredentials()로 로컬루프백 연결을 하지만, 파드 외부로 나가는 트래픽은 사이드카가 mTLS로 암호화·검증한다.
서비스 메시 방식의 장점은 인증서 회전(rotation)이 자동화된다는 점이다. 인증서가 만료되기 전에 istiod가 새 인증서를 발급하고 사이드카에 배포한다. 애플리케이션 재시작 없이 인증서가 갱신된다.
인가(Authorization): 인증 이후의 접근 제어
인증이 "누구인지" 확인이라면, 인가는 "무엇을 허용할지" 결정이다. gRPC는 별도의 인가 프레임워크를 제공하지 않는다. 인터셉터에서 JWT claims를 꺼내 메서드별 정책을 직접 검사하거나, OPA(Open Policy Agent) 같은 외부 정책 엔진과 연동한다.
간단한 역할 기반 접근 제어(RBAC) 패턴:
func authzInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
principal := ctx.Value(principalKey).(string) // 인증 인터셉터가 주입
method := info.FullMethod // "/order.v1.OrderService/DeleteOrder"
if !policy.Allow(principal, method) {
return nil, status.Errorf(codes.PermissionDenied,
"principal %s is not allowed to call %s", principal, method)
}
return handler(ctx, req)
}UNAUTHENTICATED와 PERMISSION_DENIED 구분은 중요하다. 전자는 "누군지 모르겠음", 후자는 "누군지는 알지만 허용하지 않음"이다. 클라이언트와 모니터링 시스템이 두 상황을 다르게 처리해야 하므로 status code를 혼용하면 안 된다.
보안 설계 체크리스트
- 운영 환경에서
insecure.NewCredentials()는 절대 쓰지 않는다. TLS 없이 metadata로 JWT를 보내면 토큰이 노출된다. RequireTransportSecurity() bool이true를 반환하도록 콜 크레덴셜을 구현한다. 평문 채널에서 토큰이 의도치 않게 전송되는 상황을 방지한다.- 서버 인증서의 CN/SAN이 클라이언트가 연결하는 호스트명과 일치하는지 확인한다. 불일치는 handshake 실패 원인이다.
- JWT는 서명 알고리즘을 명시적으로 검증하고
alg: none을 허용하지 않는다.exp,iss,aud클레임을 모두 확인한다. - mTLS에서 클라이언트 인증서 갱신을 자동화한다. 인증서 만료는 갑작스러운 전체 서비스 중단으로 이어진다.
- 서비스 메시(Istio)를 쓴다면
PeerAuthentication: STRICT를 설정해 메시 내부에서도 평문 gRPC를 차단한다. - 인터셉터 순서를 설계할 때 인증 → 인가 순서를 유지한다. 인가 인터셉터가 인증 전에 실행되면 principal 정보가 없어 모든 요청이
PERMISSION_DENIED가 된다. - 에러에 내부 원인을 노출하지 않는다. 인증 실패 이유를 클라이언트에 상세히 알리면 공격자에게 정보가 된다.