Skip to content

youngho98/mtls-auth-system

Repository files navigation

mTLS 인증 시스템

Go 언어로 구현한 mTLS(Mutual TLS) 기반 인증 시스템입니다.

클라이언트 인증서를 사용한 상호 인증과 JWT 토큰 기반 API 접근 제어를 구현합니다.

누구나 Docker만 있으면 바로 실행하고 테스트할 수 있습니다.


주요 특징

  • mTLS (Mutual TLS): 서버와 클라이언트 양방향 인증서 검증
  • JWT 토큰 발급: 인증된 클라이언트에게 1시간 유효 토큰 발급
  • Redis 토큰 저장: TTL 기반 자동 만료 관리
  • 웹 UI 제공: 인증서 발급부터 테스트까지 브라우저에서 완료
  • Docker 기반: 5개 서비스가 격리된 네트워크에서 동작
  • 최소 외부 노출: UI 포트만 외부 노출, 인증 서비스는 내부망 전용

시스템 아키텍처

graph TB
    subgraph "외부 접근 (브라우저)"
        USER[사용자]
    end

    subgraph "Docker Network (mtls-network)"
        subgraph "UI 서비스 (외부 노출)"
            CERT[cert-issuer<br/>:8080<br/>HTTP]
            CLIENT[test-client<br/>:3000<br/>HTTP]
        end

        subgraph "인증 서비스 (내부 전용)"
            AUTH[auth-server<br/>:8443<br/>mTLS]
            REDIS[(Redis<br/>:6379)]
        end

        subgraph "API 서비스 (내부 전용)"
            TEST[test-server<br/>:9443<br/>HTTPS]
        end

        subgraph "공유 볼륨"
            CERTS[/certs/<br/>ca/ server/ client/]
        end
    end

    USER -->|"http://localhost:8080"| CERT
    USER -->|"http://localhost:3000"| CLIENT

    CERT -->|인증서 발급| CERTS
    CLIENT -->|mTLS + 클라이언트 인증서| AUTH
    CLIENT -->|Bearer JWT| TEST
    AUTH <-->|토큰 저장/조회| REDIS

    AUTH -.->|인증서 로드| CERTS
    TEST -.->|인증서 로드| CERTS
    CLIENT -.->|인증서 로드| CERTS

    style CERT fill:#e1f5fe
    style CLIENT fill:#e1f5fe
    style AUTH fill:#fff3e0
    style TEST fill:#e8f5e9
    style REDIS fill:#fce4ec
Loading

인증 플로우

sequenceDiagram
    autonumber
    participant C as Test Client
    participant A as Auth Server
    participant R as Redis
    participant T as Test Server

    Note over C,A: Step 1: mTLS 인증 + JWT 발급
    C->>A: POST /token (클라이언트 인증서)
    A->>A: 인증서 CN 추출 (test-client)
    A->>R: 기존 토큰 조회 (token:test-client)
    alt 유효한 토큰 존재
        R-->>A: 기존 JWT 반환
    else 토큰 없음/만료
        A->>A: 새 JWT 생성 (1시간 유효)
        A->>R: 토큰 저장 (TTL: 1시간)
    end
    A-->>C: JWT 토큰 반환

    Note over C,T: Step 2: JWT로 API 호출
    C->>T: GET /api/data (Bearer JWT)
    T->>T: JWT 검증
    T-->>C: 데이터 반환
Loading

인증서 구조

graph TD
    CA[Root CA<br/>ca/ca.pem<br/>유효기간: 10년]

    CA -->|서명| SERVER[서버 인증서<br/>server/cert.pem<br/>유효기간: 1년]
    CA -->|서명| CLIENT_CERT[클라이언트 인증서<br/>client/cert.pem<br/>유효기간: 1년]

    SERVER -->|사용| AUTH_USE[auth-server]
    SERVER -->|사용| TEST_USE[test-server]
    CLIENT_CERT -->|사용| CLIENT_USE[test-client]

    style CA fill:#ffcdd2
    style SERVER fill:#c8e6c9
    style CLIENT_CERT fill:#bbdefb
Loading
인증서 경로 용도 DNS SANs
Root CA certs/ca/ca.pem 모든 인증서 서명 -
서버 인증서 certs/server/cert.pem auth-server, test-server 신원 증명 auth-server, test-server, localhost 등
클라이언트 인증서 certs/client/cert.pem test-client 신원 증명 -

서비스 구성

서비스 포트 프로토콜 외부 노출 역할
cert-issuer 8080 HTTP O CA 생성, 인증서 발급 UI
test-client 3000 HTTP O mTLS 테스트 UI
auth-server 8443 HTTPS/mTLS X 클라이언트 인증 + JWT 발급
test-server 9443 HTTPS X JWT 검증 + 보호된 API
redis 6379 TCP X JWT 토큰 저장소 (TTL: 1시간)

빠른 시작

사전 요구사항

  • Docker 20.10+
  • Docker Compose 2.0+

1. 서비스 시작

git clone https://github.com/youngho98/mtls-auth-system.git
cd mtls-auth-system
docker compose up -d --build

초기에는 auth-server, test-server, test-client가 인증서 없이 재시작을 반복합니다. 정상입니다.

2. 인증서 발급

http://localhost:8080 접속

  1. Root CA 생성 버튼 클릭
  2. 서버 인증서 발급 버튼 클릭 → certs/server/에 자동 저장
  3. 클라이언트 인증서 발급 버튼 클릭 → certs/client/에 자동 저장

3. 서비스 재시작

docker compose restart auth-server test-server test-client

4. 테스트

http://localhost:3000 접속 → 테스트 요청 버튼 클릭

graph LR
    A[테스트 요청] --> B{Step 1: 토큰 획득}
    B -->|성공| C{Step 2: 데이터 조회}
    C -->|성공| D[테스트 성공!]
    B -->|실패| E[테스트 실패]
    C -->|실패| E

    style D fill:#c8e6c9
    style E fill:#ffcdd2
Loading

기술 스택

기술 용도 버전
Go 백엔드 서버 1.25
Redis 토큰 저장소 7 (Alpine)
Docker 컨테이너화 20.10+
JWT 인증 토큰 HS256
mTLS 상호 인증 TLS 1.2+

왜 Go인가?

1. 표준 라이브러리만으로 TLS/인증서 처리 가능

// 외부 OpenSSL 의존성 없이 인증서 생성
import (
    "crypto/tls"
    "crypto/x509"
    "crypto/rsa"
)
  • crypto/tls: TLS 설정 및 mTLS 구현
  • crypto/x509: 인증서 생성, 서명, 검증
  • crypto/rsa: RSA 키 생성
  • 외부 라이브러리 의존성 최소화

2. 단일 바이너리 배포

# 멀티스테이지 빌드
FROM golang:1.25-alpine AS builder
RUN CGO_ENABLED=0 go build -o /app ./cmd/auth-server

FROM scratch
COPY --from=builder /app /app
# 결과: ~15MB 경량 컨테이너

3. 동시성 처리

// 고루틴 기반 효율적인 연결 처리
// 100+ 동시 요청 처리 가능
http.ListenAndServeTLS(":8443", certFile, keyFile, handler)

왜 mTLS인가?

인증 방식 장점 단점
ID/Password 구현 간단 유출 시 무방비, 브루트포스 공격 취약
API Key 간단, 서버간 통신에 적합 키 관리 필요, 탈취 시 위험
OAuth 2.0 표준화, 세분화된 권한 복잡한 플로우
mTLS 양방향 신뢰, 인증서 기반 인증서 관리 필요

mTLS 선택 이유:

  • 클라이언트 신원을 암호학적으로 보장
  • 인증서 만료/폐기를 통한 접근 제어
  • 네트워크 레벨에서 인증 (애플리케이션 코드 이전에 검증)
  • 내부 마이크로서비스 간 통신에 적합

왜 JWT인가?

mTLS로 인증 → JWT 발급 → JWT로 API 호출
  • Stateless: 서버가 세션을 저장할 필요 없음
  • 확장성: 여러 서버에서 동일한 시크릿으로 검증 가능
  • 표준화: RFC 7519, 다양한 언어/프레임워크 지원
  • Redis 활용: 토큰 재사용, TTL 기반 자동 만료

핵심 구현 포인트

1. mTLS 서버 설정

// pkg/mtls/config.go
tlsConfig := &tls.Config{
    ClientAuth: tls.RequireAndVerifyClientCert,  // 클라이언트 인증서 필수
    ClientCAs:  caCertPool,                       // CA로 클라이언트 검증
    MinVersion: tls.VersionTLS12,
}

2. 클라이언트 CN 추출

// cmd/auth-server/main.go
func getClientCN(r *http.Request) string {
    if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
        return r.TLS.PeerCertificates[0].Subject.CommonName
    }
    return ""
}

3. JWT 토큰 생성/검증

// pkg/jwt/token.go
claims := &Claims{
    Subject:   cn,                              // 클라이언트 CN
    IssuedAt:  time.Now().Unix(),
    ExpiresAt: time.Now().Add(time.Hour).Unix(), // 1시간 유효
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

4. Redis TTL 관리

// pkg/jwt/storage.go
// 토큰 저장 시 TTL 설정 → 1시간 후 자동 삭제
err := client.Set(ctx, "token:"+cn, jwtToken, time.Hour).Err()

5. X.509 인증서 생성

// pkg/cert/generator.go
template := &x509.Certificate{
    SerialNumber: big.NewInt(time.Now().UnixNano()),
    Subject:      pkix.Name{CommonName: cn},
    NotBefore:    time.Now(),
    NotAfter:     time.Now().AddDate(1, 0, 0), // 1년 유효
    DNSNames:     []string{"auth-server", "test-server", "localhost"},
}
certDER, _ := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey)

API 엔드포인트

Auth Server (내부: 8443)

엔드포인트 메서드 인증 설명
/token POST mTLS JWT 토큰 발급
/health GET 없음 헬스체크 (Redis 상태)
# JWT 토큰 발급 (mTLS 필요)
curl -X POST https://localhost:8443/token \
    --cert certs/client/cert.pem \
    --key certs/client/key.pem \
    --cacert certs/ca/ca.pem
{
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "expires_in": 3600,
    "token_type": "Bearer"
}

Test Server (내부: 9443)

엔드포인트 메서드 인증 설명
/api/data GET Bearer JWT 보호된 데이터 조회
/health GET 없음 헬스체크
# 보호된 API 호출 (Bearer 토큰 필요)
curl https://localhost:9443/api/data \
    --cacert certs/ca/ca.pem \
    -H "Authorization: Bearer $TOKEN"
{
    "message": "성공!",
    "data": {
        "timestamp": "2024-01-01T12:00:00Z",
        "client": "test-client"
    }
}

Cert Issuer (외부: 8080)

엔드포인트 메서드 설명
/ GET 웹 UI
/api/ca/generate POST Root CA 생성
/api/ca/status GET CA 존재 여부
/api/cert/issue POST 인증서 발급

프로젝트 구조

mtls-auth-system/
├── cmd/                          # 서버 진입점
│   ├── auth-server/main.go       # mTLS 인증 + JWT 발급
│   ├── cert-issuer/main.go       # 인증서 발급 웹 서비스
│   ├── test-server/main.go       # JWT 검증 API
│   └── test-client/main.go       # 테스트 웹 클라이언트
│
├── pkg/                          # 공유 라이브러리
│   ├── cert/generator.go         # X.509 인증서 생성/서명
│   ├── config/config.go          # YAML 설정 로더
│   ├── jwt/
│   │   ├── token.go              # JWT 생성/검증
│   │   └── storage.go            # Redis 토큰 저장
│   └── mtls/config.go            # TLS 설정 유틸리티
│
├── web/static/                   # 웹 UI
│   ├── index.html                # cert-issuer UI
│   └── client.html               # test-client UI
│
├── configs/                      # 서비스별 YAML 설정
│   ├── auth-server.yaml
│   ├── cert-issuer.yaml
│   ├── test-server.yaml
│   └── test-client.yaml
│
├── certs/                        # 인증서 저장 (gitignore)
│   ├── ca/                       # Root CA
│   ├── server/                   # 서버 인증서
│   └── client/                   # 클라이언트 인증서
│
├── docker-compose.yml            # 서비스 오케스트레이션
├── Dockerfile.auth-server
├── Dockerfile.cert-issuer
├── Dockerfile.test-server
├── Dockerfile.test-client
├── go.mod
└── go.sum

보안 고려사항

현재 구현

항목 상태 설명
mTLS 클라이언트 인증 RequireAndVerifyClientCert
JWT 서명 HS256, 공유 시크릿
토큰 자동 만료 Redis TTL 1시간
내부 서비스 격리 UI 포트만 외부 노출
TLS 1.2+ 강제 MinVersion 설정

프로덕션 적용 시 권장사항

항목 현재 권장
JWT 시크릿 설정 파일 환경 변수 또는 Vault
서버 인증서 서버간 공유 서버별 분리
Redis 비밀번호 없음 비밀번호 + TLS
인증서 갱신 수동 자동화 (cert-manager)
로깅 stdout 중앙 로그 수집

문제 해결

증상 원인 해결
서비스가 재시작 반복 인증서 없음 인증서 발급 후 재시작
"certificate signed by unknown authority" CA 불일치 같은 CA로 서명된 인증서 사용
"remote error: tls: bad certificate" 클라이언트 인증서 문제 클라이언트 인증서 재발급
"토큰이 만료되었습니다" JWT 1시간 만료 새 토큰 요청
Redis 연결 실패 컨테이너 미실행 docker compose up -d redis

로그 확인

docker compose logs auth-server
docker compose logs test-server
docker compose logs test-client

인증서 검증

# 인증서 정보 확인
openssl x509 -in certs/server/cert.pem -noout -text

# 인증서 체인 검증
openssl verify -CAfile certs/ca/ca.pem certs/server/cert.pem

# 인증서 만료일 확인
openssl x509 -in certs/client/cert.pem -noout -dates

전체 초기화

docker compose down
rm -rf certs/*
docker compose up -d --build
# 이후 인증서 재발급

확장 가능성

이 프로젝트를 기반으로 다음과 같이 확장할 수 있습니다:

확장 설명
다중 클라이언트 여러 CN으로 클라이언트 인증서 발급, 각각 별도 토큰 관리
역할 기반 접근 제어 JWT claims에 역할 추가, 엔드포인트별 권한 검사
인증서 폐기 목록 (CRL) 폐기된 인증서 목록 관리, 실시간 검증
OCSP 온라인 인증서 상태 프로토콜로 실시간 유효성 검증
Kubernetes 배포 Helm 차트, cert-manager 연동

상세 문서


의존성

require (
    github.com/golang-jwt/jwt/v5 v5.2.0  // JWT 토큰 처리
    github.com/go-redis/redis/v8 v8.11.5  // Redis 클라이언트
    gopkg.in/yaml.v3 v3.0.1               // YAML 설정 파싱
)

최소한의 외부 의존성으로 유지보수성 확보


라이선스

MIT License


기여

  1. Fork 저장소
  2. Feature branch 생성 (git checkout -b feature/amazing)
  3. Commit (git commit -m 'feat: Add amazing feature')
  4. Push (git push origin feature/amazing)
  5. Pull Request 생성

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors