본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 8장: API 인증과 서비스 간 통신 보안
2026년 6월 29일·아키텍처·

8장: API 인증과 서비스 간 통신 보안

API 키 관리, OAuth Client Credentials, mTLS를 활용한 서비스 간 인증, 마이크로서비스에서의 JWT 전파, API 게이트웨이 인증 패턴을 실전 코드와 함께 설명합니다.

18분1,105자1개 섹션
securityprotocoldesign-patternsinfrastructureperformance
공유
modern-auth8 / 10
12345678910
이전7장: 제로 트러스트 아키텍처와 인증다음9장: 규제 대응과 컴플라이언스

API 인증과 서비스 간 통신 보안

사용자 인증이 "사람이 시스템에 접근하는 것"을 다룬다면, API 인증과 서비스 간 통신 보안은 "시스템이 시스템에 접근하는 것"을 다룹니다. 마이크로서비스 아키텍처에서는 수십, 수백 개의 서비스가 서로를 호출하며, 각 호출마다 "이 서비스가 이 요청을 할 자격이 있는가?"를 검증해야 합니다.

API 키 관리

API 키(API Key)는 가장 단순한 API 인증 수단입니다. 클라이언트를 식별하는 긴 문자열을 헤더나 쿼리 파라미터로 전달합니다.

API 키의 동작

API 키를 사용한 요청
http
GET /api/v1/data HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0

API 키 생성과 관리

api-key-management.ts
typescript
import crypto from "crypto";
 
interface ApiKey {
  id: string;
  hashedKey: string;       // 키의 해시값만 저장
  prefix: string;          // 식별용 접두어 (sk_live_a1b2)
  name: string;            // 키 이름 (사용처 설명)
  clientId: string;        // 소유자
  scopes: string[];        // 허용 범위
  rateLimit: number;       // 분당 요청 제한
  expiresAt: Date | null;  // 만료일 (null이면 무기한)
  lastUsedAt: Date | null;
  createdAt: Date;
}
 
function generateApiKey(): { key: string; prefix: string; hash: string } {
  // 32바이트 랜덤 키 생성
  const rawKey = crypto.randomBytes(32).toString("base64url");
  const key = `sk_live_${rawKey}`;
  const prefix = key.substring(0, 12);
  const hash = crypto
    .createHash("sha256")
    .update(key)
    .digest("hex");
 
  return { key, prefix, hash };
}
 
async function validateApiKey(
  providedKey: string,
): Promise<ApiKey | null> {
  const hash = crypto
    .createHash("sha256")
    .update(providedKey)
    .digest("hex");
 
  const apiKey = await findApiKeyByHash(hash);
 
  if (!apiKey) return null;
 
  // 만료 확인
  if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
    return null;
  }
 
  // 사용 시간 업데이트
  await updateLastUsed(apiKey.id);
 
  return apiKey;
}
Warning

API 키는 인증(Authentication)보다는 식별(Identification)에 가깝습니다. API 키만으로는 "누가" 요청하는지 알 수 있지만, 그 키가 정당한 소유자에 의해 사용되는지는 알 수 없습니다. 민감한 API에는 API 키 단독 사용을 피하고, OAuth 2.0이나 mTLS와 조합하는 것이 좋습니다.

API 키 보안 모범 사례

원칙설명
해시 저장키의 해시값만 DB에 저장. 원본은 생성 시 한 번만 표시
범위 제한키마다 허용하는 API 범위(scope)를 최소화
만료 설정무기한 키를 피하고, 주기적 로테이션을 권장
속도 제한키별 요청 속도(rate limit)를 설정하여 남용 방지
환경 분리테스트 키와 프로덕션 키를 명확히 분리
사용 모니터링비정상 사용 패턴 감지 (갑작스런 트래픽 증가 등)

OAuth 2.0 Client Credentials

서비스 간 통신에서는 사용자 개입이 없으므로, Client Credentials Grant를 사용합니다. 서비스 자체가 클라이언트이자 리소스 요청자입니다.

client-credentials.ts
typescript
interface ServiceTokenCache {
  token: string;
  expiresAt: number;
}
 
// 서비스 토큰 캐시 (토큰 재사용)
const tokenCache = new Map<string, ServiceTokenCache>();
 
async function getServiceToken(
  targetService: string,
  scopes: string[],
): Promise<string> {
  const cacheKey = `${targetService}:${scopes.join(",")}`;
  const cached = tokenCache.get(cacheKey);
 
  // 만료 1분 전까지 캐시된 토큰 재사용
  if (cached && cached.expiresAt > Date.now() + 60000) {
    return cached.token;
  }
 
  const response = await fetch(AUTH_SERVER_TOKEN_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`,
    },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      scope: scopes.join(" "),
      audience: `https://${targetService}.internal`,
    }),
  });
 
  const data = await response.json();
 
  tokenCache.set(cacheKey, {
    token: data.access_token,
    expiresAt: Date.now() + data.expires_in * 1000,
  });
 
  return data.access_token;
}
 
// 서비스 간 호출 시 사용
async function callOrderService(orderId: string) {
  const token = await getServiceToken("order-service", ["orders:read"]);
 
  return fetch(`https://order-service.internal/api/orders/${orderId}`, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
}

mTLS: 상호 TLS 인증

mTLS(mutual TLS)는 서버뿐 아니라 클라이언트도 인증서로 자신을 증명하는 양방향 TLS 인증입니다. 서비스 간 통신에서 가장 강력한 인증 수단 중 하나입니다.

mTLS 설정 예시 (Node.js)

mtls-server.ts
typescript
import https from "https";
import fs from "fs";
 
// 서버 측 설정
const serverOptions: https.ServerOptions = {
  // 서버 인증서
  key: fs.readFileSync("/certs/server-key.pem"),
  cert: fs.readFileSync("/certs/server-cert.pem"),
 
  // CA 인증서 (클라이언트 인증서 검증용)
  ca: fs.readFileSync("/certs/ca-cert.pem"),
 
  // 클라이언트 인증서 요구
  requestCert: true,
  rejectUnauthorized: true,
};
 
const server = https.createServer(serverOptions, (req, res) => {
  // 클라이언트 인증서 정보 추출
  const clientCert = req.socket.getPeerCertificate();
 
  if (!clientCert || !clientCert.subject) {
    res.writeHead(403);
    res.end("클라이언트 인증서가 필요합니다.");
    return;
  }
 
  // 인증서의 CN(Common Name)으로 서비스 식별
  const serviceName = clientCert.subject.CN;
  console.log(`인증된 서비스: ${serviceName}`);
 
  // 서비스별 권한 확인
  if (!isAuthorized(serviceName, req.url ?? "", req.method ?? "")) {
    res.writeHead(403);
    res.end("해당 작업에 대한 권한이 없습니다.");
    return;
  }
 
  // 요청 처리
  handleRequest(req, res);
});
mtls-client.ts
typescript
import https from "https";
import fs from "fs";
 
// 클라이언트 측 설정
const clientOptions: https.RequestOptions = {
  hostname: "order-service.internal",
  port: 443,
  path: "/api/orders",
  method: "GET",
 
  // 클라이언트 인증서
  key: fs.readFileSync("/certs/client-key.pem"),
  cert: fs.readFileSync("/certs/client-cert.pem"),
 
  // 서버 CA 인증서
  ca: fs.readFileSync("/certs/ca-cert.pem"),
};
 
const req = https.request(clientOptions, (res) => {
  let data = "";
  res.on("data", (chunk) => { data += chunk; });
  res.on("end", () => {
    console.log("응답:", JSON.parse(data));
  });
});
req.end();
Tip

mTLS의 인증서 관리는 복잡할 수 있습니다. 쿠버네티스 환경에서는 서비스 메시(Istio, Linkerd)가 mTLS를 자동으로 처리해줍니다. 인증서 발급, 로테이션, 폐기를 사이드카 프록시가 투명하게 관리하므로, 애플리케이션 코드를 수정할 필요가 없습니다.

마이크로서비스에서의 JWT 전파

사용자가 API 게이트웨이를 통해 요청하면, 그 요청이 여러 내부 서비스를 거칠 수 있습니다. 이때 사용자의 인증 정보(JWT)를 어떻게 전파할지가 중요한 설계 결정입니다.

패턴 1: 토큰 그대로 전파

  • 장점: 구현이 단순합니다
  • 단점: 모든 서비스가 사용자 토큰에 접근. 최소 권한 원칙 위반

패턴 2: 토큰 교환(Token Exchange)

토큰 교환(Token Exchange, RFC 8693)을 사용하면 서비스가 인가 서버에 현재 토큰을 제출하고, 대상 서비스에 맞는 범위가 축소된 새 토큰을 받습니다.

token-exchange.ts
typescript
async function exchangeToken(config: {
  subjectToken: string;       // 현재 가진 토큰
  targetAudience: string;     // 대상 서비스
  requestedScopes: string[];  // 필요한 범위
}): Promise<string> {
  const response = await fetch(AUTH_SERVER_TOKEN_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`,
    },
    body: new URLSearchParams({
      grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
      subject_token: config.subjectToken,
      subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
      audience: config.targetAudience,
      scope: config.requestedScopes.join(" "),
    }),
  });
 
  const data = await response.json();
  return data.access_token;
}
 
// 서비스 A에서 서비스 B 호출 시
async function callServiceB(userToken: string, orderId: string) {
  // 사용자 토큰을 서비스 B 전용 토큰으로 교환
  const serviceBToken = await exchangeToken({
    subjectToken: userToken,
    targetAudience: "https://service-b.internal",
    requestedScopes: ["orders:read"],
  });
 
  return fetch(`https://service-b.internal/api/orders/${orderId}`, {
    headers: { Authorization: `Bearer ${serviceBToken}` },
  });
}

패턴 3: 사용자 컨텍스트 + 서비스 인증 분리

이 패턴에서는 서비스 간 인증은 mTLS로 처리하고, 사용자 정보는 서명된 컨텍스트 헤더로 전파합니다.

user-context-propagation.ts
typescript
import { SignJWT, jwtVerify } from "jose";
 
// API 게이트웨이: 사용자 컨텍스트 생성
async function createUserContext(
  userToken: JwtPayload,
): Promise<string> {
  // 필요한 최소 정보만 포함
  const context = await new SignJWT({
    sub: userToken.sub,
    roles: userToken.roles,
    tenant_id: userToken.tenant_id,
    // 원본 토큰의 만료 시간을 유지
    original_exp: userToken.exp,
  })
    .setProtectedHeader({ alg: "ES256" })
    .setIssuedAt()
    .setExpirationTime("5m") // 짧은 수명
    .setIssuer("api-gateway")
    .sign(GATEWAY_PRIVATE_KEY);
 
  return context;
}
 
// 내부 서비스: 사용자 컨텍스트 검증
async function verifyUserContext(
  contextHeader: string,
): Promise<UserContext> {
  const { payload } = await jwtVerify(
    contextHeader,
    GATEWAY_PUBLIC_KEY,
    {
      issuer: "api-gateway",
      algorithms: ["ES256"],
    }
  );
 
  return {
    userId: payload.sub as string,
    roles: payload.roles as string[],
    tenantId: payload.tenant_id as string,
  };
}

API 게이트웨이 인증 패턴

API 게이트웨이(API Gateway)는 모든 외부 요청의 단일 진입점으로, 인증을 중앙에서 처리하는 핵심 컴포넌트입니다.

게이트웨이 인증 미들웨어

gateway-auth-middleware.ts
typescript
import type { Request, Response, NextFunction } from "express";
 
interface AuthConfig {
  publicPaths: string[];     // 인증 불필요 경로
  apiKeyPaths: string[];     // API 키 인증 경로
  oauthPaths: string[];      // OAuth 인증 경로
}
 
function gatewayAuthMiddleware(config: AuthConfig) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const path = req.path;
 
    // 1. 공개 경로 확인
    if (config.publicPaths.some((p) => path.startsWith(p))) {
      return next();
    }
 
    // 2. API 키 인증 경로
    if (config.apiKeyPaths.some((p) => path.startsWith(p))) {
      const apiKey = req.headers["x-api-key"] as string;
      if (!apiKey) {
        return res.status(401).json({ error: "API 키가 필요합니다." });
      }
 
      const keyData = await validateApiKey(apiKey);
      if (!keyData) {
        return res.status(401).json({ error: "유효하지 않은 API 키입니다." });
      }
 
      // 스코프 확인
      const requiredScope = getRequiredScope(req.method, path);
      if (!keyData.scopes.includes(requiredScope)) {
        return res.status(403).json({ error: "해당 API에 대한 권한이 없습니다." });
      }
 
      req.headers["x-client-id"] = keyData.clientId;
      req.headers["x-auth-method"] = "api-key";
      return next();
    }
 
    // 3. OAuth 토큰 인증
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith("Bearer ")) {
      return res.status(401).json({ error: "Bearer 토큰이 필요합니다." });
    }
 
    const token = authHeader.substring(7);
 
    try {
      const payload = await verifyAccessToken(token);
 
      // 사용자 정보를 헤더로 전달
      req.headers["x-user-id"] = payload.sub;
      req.headers["x-user-roles"] = JSON.stringify(payload.roles);
      req.headers["x-tenant-id"] = payload.tenant_id;
      req.headers["x-auth-method"] = "oauth";
 
      // Authorization 헤더 제거 (내부 서비스에 원본 토큰 전달 안 함)
      delete req.headers.authorization;
 
      next();
    } catch (error) {
      if (error instanceof TokenExpiredError) {
        return res.status(401).json({ error: "토큰이 만료되었습니다." });
      }
      return res.status(401).json({ error: "유효하지 않은 토큰입니다." });
    }
  };
}
Info

API 게이트웨이에서 인증을 처리하면 내부 서비스는 인증 로직에서 해방됩니다. 내부 서비스는 게이트웨이가 전달한 헤더(x-user-id, x-user-roles 등)만 신뢰하면 됩니다. 단, 내부 서비스 간 직접 통신이 있다면 mTLS 등의 별도 인증이 필요합니다.

인증 방식 선택 매트릭스

시나리오권장 방식이유
사용자 -> 서비스OAuth 2.0 + PKCE표준화된 위임 인증
서비스 -> 서비스 (내부)mTLS 또는 Client Credentials강력한 양방향 인증
서드파티 -> APIAPI 키 + OAuth식별 + 인증 조합
서비스 메시 내부mTLS (자동, Istio/Linkerd)투명한 인증서 관리
웹훅(Webhook) 수신HMAC 서명 검증발신자 위조 방지

마치며

API 인증과 서비스 간 통신 보안은 마이크로서비스 아키텍처의 근간입니다. API 키는 단순한 식별에, OAuth Client Credentials는 표준화된 서비스 인증에, mTLS는 가장 강력한 양방향 인증에 사용됩니다. JWT 전파 전략은 보안, 성능, 운영 복잡도의 균형을 고려하여 선택해야 하며, API 게이트웨이는 인증을 중앙화하여 내부 서비스의 부담을 줄여줍니다.

다음 장에서는 인증 시스템에 적용되는 규제와 컴플라이언스(Compliance) 요구사항을 다룹니다. GDPR, PCI-DSS, SOC 2가 인증에 부과하는 구체적 요구사항을 살펴보겠습니다.

이 글이 도움이 되셨나요?

관련 글

아키텍처

7장: 제로 트러스트 아키텍처와 인증

제로 트러스트의 핵심 원칙과 인증의 관계를 분석하고, 지속적 검증, 디바이스 신뢰, 컨텍스트 기반 접근 제어, BeyondCorp 모델, 아이덴티티 인식 프록시를 설명합니다.

2026년 6월 27일·17분
아키텍처

9장: 규제 대응과 컴플라이언스

GDPR, PCI-DSS, SOC 2가 인증 시스템에 부과하는 구체적 요구사항, NIST 800-63B 비밀번호 가이드라인, 감사 로깅 설계, MFA 요구사항, 데이터 거주 전략을 정리합니다.

2026년 7월 2일·21분
아키텍처

6장: 토큰 관리와 세션 전략

JWT의 클레임과 서명 알고리즘을 깊이 분석하고, 리프레시 토큰 로테이션, 토큰 저장 전략, 세션 관리 패턴, BFF 패턴을 실전 코드와 함께 설명합니다.

2026년 6월 24일·19분
이전 글7장: 제로 트러스트 아키텍처와 인증
다음 글9장: 규제 대응과 컴플라이언스

댓글

목차

약 18분 남음
  • API 인증과 서비스 간 통신 보안
    • API 키 관리
      • API 키의 동작
      • API 키 생성과 관리
      • API 키 보안 모범 사례
    • OAuth 2.0 Client Credentials
    • mTLS: 상호 TLS 인증
      • mTLS 설정 예시 (Node.js)
    • 마이크로서비스에서의 JWT 전파
      • 패턴 1: 토큰 그대로 전파
      • 패턴 2: 토큰 교환(Token Exchange)
      • 패턴 3: 사용자 컨텍스트 + 서비스 인증 분리
    • API 게이트웨이 인증 패턴
      • 게이트웨이 인증 미들웨어
    • 인증 방식 선택 매트릭스
    • 마치며