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
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: 데이터 반환
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
| 인증서 | 경로 | 용도 | 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+
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가 인증서 없이 재시작을 반복합니다. 정상입니다.
- Root CA 생성 버튼 클릭
- 서버 인증서 발급 버튼 클릭 →
certs/server/에 자동 저장 - 클라이언트 인증서 발급 버튼 클릭 →
certs/client/에 자동 저장
docker compose restart auth-server test-server test-clienthttp://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
| 기술 | 용도 | 버전 |
|---|---|---|
| Go | 백엔드 서버 | 1.25 |
| Redis | 토큰 저장소 | 7 (Alpine) |
| Docker | 컨테이너화 | 20.10+ |
| JWT | 인증 토큰 | HS256 |
| mTLS | 상호 인증 | TLS 1.2+ |
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)| 인증 방식 | 장점 | 단점 |
|---|---|---|
| ID/Password | 구현 간단 | 유출 시 무방비, 브루트포스 공격 취약 |
| API Key | 간단, 서버간 통신에 적합 | 키 관리 필요, 탈취 시 위험 |
| OAuth 2.0 | 표준화, 세분화된 권한 | 복잡한 플로우 |
| mTLS | 양방향 신뢰, 인증서 기반 | 인증서 관리 필요 |
mTLS 선택 이유:
- 클라이언트 신원을 암호학적으로 보장
- 인증서 만료/폐기를 통한 접근 제어
- 네트워크 레벨에서 인증 (애플리케이션 코드 이전에 검증)
- 내부 마이크로서비스 간 통신에 적합
mTLS로 인증 → JWT 발급 → JWT로 API 호출
- Stateless: 서버가 세션을 저장할 필요 없음
- 확장성: 여러 서버에서 동일한 시크릿으로 검증 가능
- 표준화: RFC 7519, 다양한 언어/프레임워크 지원
- Redis 활용: 토큰 재사용, TTL 기반 자동 만료
// pkg/mtls/config.go
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert, // 클라이언트 인증서 필수
ClientCAs: caCertPool, // CA로 클라이언트 검증
MinVersion: tls.VersionTLS12,
}// 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 ""
}// 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)// pkg/jwt/storage.go
// 토큰 저장 시 TTL 설정 → 1시간 후 자동 삭제
err := client.Set(ctx, "token:"+cn, jwtToken, time.Hour).Err()// 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)| 엔드포인트 | 메서드 | 인증 | 설명 |
|---|---|---|---|
/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"
}| 엔드포인트 | 메서드 | 인증 | 설명 |
|---|---|---|---|
/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"
}
}| 엔드포인트 | 메서드 | 설명 |
|---|---|---|
/ |
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 -datesdocker compose down
rm -rf certs/*
docker compose up -d --build
# 이후 인증서 재발급이 프로젝트를 기반으로 다음과 같이 확장할 수 있습니다:
| 확장 | 설명 |
|---|---|
| 다중 클라이언트 | 여러 CN으로 클라이언트 인증서 발급, 각각 별도 토큰 관리 |
| 역할 기반 접근 제어 | JWT claims에 역할 추가, 엔드포인트별 권한 검사 |
| 인증서 폐기 목록 (CRL) | 폐기된 인증서 목록 관리, 실시간 검증 |
| OCSP | 온라인 인증서 상태 프로토콜로 실시간 유효성 검증 |
| Kubernetes 배포 | Helm 차트, cert-manager 연동 |
- TESTING.md - 단계별 테스트 가이드
- SERVERINFO.md - 서버 및 인증서 상세 정보
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
- Fork 저장소
- Feature branch 생성 (
git checkout -b feature/amazing) - Commit (
git commit -m 'feat: Add amazing feature') - Push (
git push origin feature/amazing) - Pull Request 생성