글 목록

Auto-Cert: 사내 해커톤


1. 프로젝트 개요

항목내용
프로젝트명Auto-Cert (SSL/TLS 인증서 관리 자동화 솔루션)
기간2025.10 ~ 2025.11 (약 2개월)
인원프론트엔드 1명 백엔드 2명
역할백엔드 개발 (인증서 생명주기 관리, 보안 아키텍처, AI 채팅 통합)
저장소GitHub 멀티 모듈 모노레포 (총 커밋 88건, Java 소스 129개 파일, 테스트 18개 파일)

목표

기업 환경에서 수십~수백 개의 SSL/TLS 인증서를 수동으로 관리하면 만료 사고, 배포 누락, 갱신 지연 등의 운영 리스크가 발생합니다. Auto-Cert는 ACME 프로토콜(Let’s Encrypt)을 통한 인증서 발급/갱신, 대상 서버로의 SSH 배포, 웹서버 재기동, 이메일 알림까지 인증서의 전체 생명주기를 하나의 플랫폼에서 자동화하는 것을 목표로 합니다. 추가로 LLM 기반 AI 채팅 인터페이스를 통해 비전문가도 자연어로 인증서를 관리할 수 있도록 접근성을 높였습니다.


2. 사용 기술 · 지식 · 도구

핵심 기술 스택

계층기술
Language / RuntimeJava 21
FrameworkSpring Boot 3.5.7
BuildGradle 8.14.3 (Groovy DSL), 멀티 모듈 구성
DatabasePostgreSQL 16, Flyway 11.17 (마이그레이션 11건)
SecuritySpring Security, OAuth2 Resource Server (JWT), BCrypt
ACMEacme4j 3.3.1, Bouncy Castle 1.78 (Let’s Encrypt 연동)
SSH 배포sshj 0.38.0 (SFTP 파일 전송, sudo 원격 명령)
AI 채팅LangChain4j 0.35.0 + OpenAI GPT (Function Calling)
이메일 알림Spring Mail + Thymeleaf 템플릿
API 문서springdoc-openapi 2.8.4 (Swagger UI)
인프라Docker Compose (PostgreSQL, pgAdmin, ACME challenge volume)
CI/CDGitHub Actions (PR 테스트 필수 실행, Docker 이미지 빌드)
암호화AES-256-GCM (인증서 개인키 암호화 저장)

설계 원칙 및 도구

  • 멀티 모듈 모노레포: 7개 Gradle 서브모듈로 관심사 분리 (common → domain → certificate-manager / server-manager / notification-manager / ai-assistant → api)

  • 이벤트 기반 모듈 통신: Spring ApplicationEvent + @TransactionalEventListener로 모듈 간 느슨한 결합


  • Flyway 버전 관리: V1~V11까지 11개의 마이그레이션 스크립트로 스키마 진화 이력 관리

3. 아키텍처 설계 및 주요 내용

3.1 시스템 아키텍처

image.png

3.2 핵심 기능별 상세

(1) ACME 프로토콜 기반 인증서 자동 발급 및 갱신

Let’s Encrypt CA와 acme4j 라이브러리를 활용하여, DNS-01 및 HTTP-01 챌린지를 지원하는 인증서 발급 파이프라인을 구현했습니다.

  • AcmeAccountService: ACME 계정 생성/관리 및 키페어 직렬화
  • AcmeOrderService: CSR 제출 → 챌린지 처리 → 인증서 수령의 전체 흐름 오케스트레이션
  • ChallengeHandlerFactory + DnsProviderFactory: 팩토리 패턴을 적용하여 HTTP-01과 DNS-01 챌린지, 그리고 Cloudflare/수동 DNS 프로바이더를 교체 가능하게 설계
  • 발급된 인증서의 개인키는 AES-256-GCM으로 암호화 후 DB에 저장하여, 평문 노출 위험을 차단

(2) SSH/SFTP 기반 원격 서버 배포 자동화

인증서 발급 후 대상 서버로의 배포를 완전히 자동화했습니다.

  • CertificateDistributionService: SSH 연결 → 인증서/개인키/체인 파일 SFTP 업로드 → Nginx 설정 테스트(nginx -t) → Graceful Reload(nginx -s reload)
  • Exponential Backoff 재시도: SSH 연결 실패 시 최대 3회까지 지수 백오프로 재시도
  • 배포 이력 추적: 모든 배포 시도를 Deployment 테이블에 기록 (상태, 소요 시간, 에러 메시지)
  • 배포 실패가 인증서 발급 자체를 롤백하지 않도록 분리하여, 나중에 수동 재배포 가능

(3) 인증서 검증 파이프라인

발급된 인증서의 유효성을 6가지 관점에서 종합 검증합니다.

  • SignatureValidator: 서명 알고리즘 및 무결성 검증
  • ValidityPeriodValidator: 유효기간 검증 (만료 임박 경고 포함)
  • ChainValidator: 인증서 체인 신뢰성 검증 (Staging 인증서 구분)
  • RevocationValidator: CRL/OCSP 기반 폐기 상태 확인
  • DomainValidator: SAN(Subject Alternative Name) 도메인 매칭
  • KeyUsageValidator: X.509 확장 키 용도(TLS 서버 인증) 검증

(4) JWT 인증 및 보안 체계

Stateless JWT 기반 인증/인가 시스템을 구축했습니다.

  • Access Token + Refresh Token 이중 토큰 구조: Access Token의 짧은 수명(분 단위)과 Refresh Token의 안전한 로테이션
  • Refresh Token Rotation & Reuse Detection: 토큰 갱신 시 기존 토큰을 즉시 폐기하고, 이미 폐기된 토큰이 재사용되면 해당 사용자의 전체 세션을 무효화 (RFC 6819 기반 토큰 탈취 대응)
  • Token Version Validator: 사용자 권한 변경/로그아웃 시 tokenVersion을 증가시켜, 기존 발급 JWT를 즉시 무효화
  • Method-level Authorization: @RequireServerAccess, @RequireCertificateAccess 커스텀 어노테이션으로 리소스 단위 접근 제어
  • User Soft Delete: @SQLDelete + @SQLRestriction 기반 논리 삭제로 데이터 보존 및 감사 추적 가능
  • 만료/폐기 Refresh Token 정리 스케줄러: 매일 새벽 3시에 만료/폐기된 토큰 일괄 삭제

(5) 이벤트 기반 이메일 알림 시스템

인증서 생명주기 이벤트에 연동된 자동 알림 체계입니다.

  • @TransactionalEventListener(phase = AFTER_COMMIT): 트랜잭션 커밋 후에만 이메일 발송하여, 롤백된 작업에 대한 오알림 방지
  • @Async: 이메일 발송을 비동기로 처리하여 메인 트랜잭션 지연 최소화
  • Thymeleaf 템플릿: 인증서 생성/갱신/삭제/만료 임박 별 HTML 이메일 템플릿
  • CertificateExpiryScheduler: 매일 오전 9시(Asia/Seoul) 만료 임박 인증서 일괄 알림

(6) LLM 기반 AI 채팅 인터페이스

LangChain4j와 OpenAI GPT를 연동하여, 자연어로 인증서를 관리할 수 있는 채팅 인터페이스를 구현했습니다.

  • Function Calling: CertificateTools(인증서 조회/생성/갱신/통계)와 ServerTools(서버 등록)를 LLM 도구로 등록하여, 사용자의 자연어 요청을 실제 API 호출로 자동 변환
  • 세션별 독립 ChatMemory: MessageWindowChatMemory로 세션별 대화 컨텍스트를 유지하여, 멀티턴 대화 지원
  • 의도 감지(Intent Detection): 서버 등록 vs 인증서 등록 등 유사한 요청을 키워드/패턴으로 정확히 구분하는 상세한 System Prompt 설계
  • 안전한 작업 제어: 삭제 같은 위험한 작업은 LLM에 도구를 제공하지 않고, 웹 UI 안내 메시지만 반환하도록 제한

4. 과정에서 겪은 기술적 문제와 해결 방법

문제 1: Refresh Token Rotation Race Condition

상황: 동일 사용자가 여러 디바이스에서 동시에 /api/v1/auth/refresh를 호출하면, read-then-write 패턴에서 두 요청 모두 같은 토큰을 유효로 판단하여 2개의 새 Refresh Token이 생성되는 경쟁 조건이 발생했습니다.

분석: 기본 트랜잭션 격리 수준(READ_COMMITTED)에서는 findByTokenHashsetRevokedAt 사이의 시간 창을 보호할 수 없었습니다. 이는 Token Replay Attack으로 이어질 수 있는 보안 취약점이었습니다.

해결: 세 가지 방안(Pessimistic Lock, Optimistic Lock + @Version, Redis 분산 락)을 비교 분석한 후, Pessimistic Locking(@Lock(PESSIMISTIC_WRITE)) 을 선택했습니다.

  • Optimistic Lock은 충돌 시 재시도가 필요하지만, refresh token rotation은 “정확히 한 번” 실행되어야 하는 연산이라 부적절
  • Redis 분산 락은 현재 단일 인스턴스 아키텍처에서 과잉 엔지니어링
  • Refresh Token은 사용자당 수분~수시간 간격으로 갱신되므로, 행 잠금의 contention이 극히 낮아 성능 영향 무시 가능

추가로, Token Reuse Detection 로직을 구현하여 이미 폐기된 토큰이 재사용되면 해당 사용자의 모든 세션을 즉시 무효화하도록 보안을 강화했습니다.

문제 2: ACME 발급 중 장애 시 상태 불일치

상황: 인증서 발급은 DB 저장(PENDING) → ACME 서버 통신(수 초~수 분) → 결과 업데이트(ACTIVE)의 흐름인데, ACME 통신 중 서버가 비정상 종료되면 인증서가 PENDING 상태로 영구 고착되는 문제를 식별했습니다. 더 심각하게는, ACME CA에서는 인증서가 발급되었지만 DB에는 반영되지 않는 인증서 누수(Certificate Leak) 가 가능했습니다.

분석: 이것은 전형적인 분산 트랜잭션 문제로, 외부 CA 서버가 XA 프로토콜을 지원하지 않아 2PC가 불가능하고, 이미 발급된 인증서를 자동 폐기하는 보상 트랜잭션은 서비스 중단 위험이 있어 Saga 패턴도 부적절했습니다.

해결 방향: Detect-and-Recover 전략을 설계했습니다.

  • DB 트랜잭션을 짧게 분리하고, 외부 API 호출을 트랜잭션 밖으로 분리
  • Stale State Recovery Scheduler가 30분 이상 PENDING 상태인 인증서를 주기적으로 감지하여 ACME 서버에서 실제 상태를 확인 후 자동 복구
  • 이를 통해 커넥션 풀 점유 시간도 30초 → 50ms로 개선 가능

문제 3: @Transactional 내부에서의 외부 API 호출로 인한 Connection Pool 고갈

상황: CertificateService.create()@Transactional 내에서 ACME 서버 통신(30~60초)을 수행하면서 DB 커넥션을 장시간 점유하여, 동시 발급 10건 초과 시 HikariCP Connection Pool이 고갈되고 다른 모든 API까지 실패하는 Cascading Failure 위험을 분석했습니다.

해결 방향: 트랜잭션 분리 패턴을 설계하여, DB 조작은 짧은 트랜잭션으로, ACME 통신은 @Async 비동기로 처리하도록 개선 방안을 수립했습니다. 이론적으로 처리량이 0.33 TPS → 200 TPS (600배)로 개선됩니다.

문제 4: Soft Delete와 Unique Constraint 충돌

상황: User 엔티티에 @SQLDelete로 soft delete를 구현했으나, soft delete된 레코드가 물리적으로 DB에 남아 있어 동일 username으로 새 사용자를 생성하면 Unique Constraint 위반 에러가 발생했습니다. @SQLRestriction은 JPA 쿼리에만 적용되고, DB 레벨 제약 조건에는 영향이 없었습니다.

해결: PostgreSQL의 Partial Unique Index를 적용하여 WHERE deleted_at IS NULL 조건으로 활성 레코드에만 유니크 제약을 적용했습니다. JPA @UniqueConstraint로 표현할 수 없으므로 Flyway 마이그레이션으로 관리합니다.


5. 본인의 역할 및 기여 내용

담당 영역

  • 인증서 생명주기 관리 모듈 (certificate-manager): ACME 프로토콜 통합, DNS-01/HTTP-01 챌린지 처리, Cloudflare DNS 연동, 인증서 검증 파이프라인(6종 Validator), 개인키 AES-256-GCM 암호화, SSH/SFTP 원격 배포 + Nginx 자동 재기동
  • 보안 아키텍처 (api/security): JWT Access/Refresh Token 이중 인증 체계, Refresh Token Rotation + Reuse Detection, Token Version 기반 즉시 무효화, User Soft Delete, 역할 기반 접근 제어
  • AI 채팅 통합 (ai-assistant): LangChain4j + OpenAI Function Calling 기반 자연어 인증서 관리 인터페이스, System Prompt 설계 및 의도 감지 로직
  • 알림 시스템 (notification-manager): 이벤트 기반 비동기 이메일 알림, 만료 임박 스케줄러
  • 아키텍처 설계: 멀티 모듈 구조 설계, 모듈 의존성 규칙 수립, Flyway 마이그레이션 전략, CI/CD 파이프라인(GitHub Actions) 구축

6. 성과와 배운 점

성과

  1. 인증서 관리 자동화 달성: Let’s Encrypt 연동을 통한 인증서 발급 → 암호화 저장 → SSH 원격 배포 → Nginx Graceful Reload → 이메일 알림까지의 전체 파이프라인을 하나의 플랫폼에서 완성
  2. 보안 수준 향상: 인증서 개인키 AES-256-GCM 암호화, JWT Refresh Token Rotation + Reuse Detection, Token Version 기반 즉시 무효화 등 실무 수준의 보안 체계 구축
  3. AI 채팅 인터페이스 제공: 비전문가도 “example.com 인증서 등록해줘”와 같은 자연어로 인증서를 관리할 수 있는 접근성 확보
  4. 운영 안정성 설계: 트러블슈팅 시나리오 8건을 사전에 분석하고, 각각에 대한 해결 방안과 트레이드오프를 문서화하여 아키텍처 개선 로드맵 수립

배운 점

  1. 분산 시스템의 일관성 문제: 외부 CA 서버와의 통합에서 트랜잭션 경계 설정, 상태 불일치 복구 전략(Detect-and-Recover), @Transactional과 외부 API 호출 분리의 중요성을 실감
  2. 보안과 사용성의 균형: Refresh Token의 Race Condition, Soft Delete와 Unique Constraint 충돌, 메모리 내 Private Key 잔존 등 실무에서 발생할 수 있는 보안 이슈를 직접 분석하며, 트레이드오프 기반 의사결정 역량 강화
  3. 멀티 모듈 설계의 가치: 7개 서브모듈로 관심사를 명확히 분리함으로써, 3명이 병렬로 개발하면서도 모듈 간 충돌을 최소화할 수 있었고, 이벤트 기반 통신으로 알림 모듈을 독립적으로 추가/제거할 수 있었음
  4. LLM의 실용적 활용: Function Calling과 System Prompt 엔지니어링을 통해 LLM을 단순 Q&A가 아닌 실제 시스템 조작 인터페이스로 활용하는 경험을 획득. 특히 의도 감지(Intent Detection)의 정확도를 높이기 위한 프롬프트 설계의 중요성을 체감

7. 기술적 의사결정 요약

결정 사항선택한 방안대안 및 탈락 사유
인증서 개인키 보관AES-256-GCM 앱 레벨 암호화HashiCorp Vault (인프라 복잡도 과다), Jasypt (암호화 수준 부족)
Token 동시성 제어Pessimistic LockOptimistic Lock (재시도 부적절), Redis 분산 락 (단일 인스턴스에서 과잉)
상태 불일치 복구Detect-and-Recover (Scheduler 폴링)2PC (외부 CA 미지원), Saga (보상 트랜잭션 부적절), Outbox (복잡도 과다)
모듈 간 통신Spring ApplicationEventKafka/RabbitMQ (현재 규모에서 과잉, 스케일아웃 시 전환 예정)
Soft Delete + UniquePostgreSQL Partial Unique IndexCompound Unique (다중 삭제 시 충돌), username 변경 (감사 추적 곤란)
AI 통합LangChain4j Function CallingREST API 직접 호출 (비전문가 사용 불가), Rule-based 챗봇 (유연성 부족)