# 평가: 보안 — SSL 인증서가 적용되어 안전한 통신이 이루어지고 있는가
> 대분류: 보안 / 소분류: SSL 인증서가 적용되어 안전한 통신이 이루어지고 있는가
> 기준 브랜치: `origin/dev` (작성 시점 HEAD: `cb41e36`)
## 요약
- **만족도**: **충족** (HTTPS는 적용되지만, 앞단 reverse proxy에 의존해 코드/리포지토리만으로는 "TLS 강제"가 보장되지 않음. HSTS · forward-headers · HTTP→HTTPS redirect 강제 정책이 코드에 명시되어 있지 않음)
### 근거
- 운영/스테이징 도메인이 모두 `https://` 스킴으로 코드 곳곳에 못박혀 있음. 즉 클라이언트–서버 트래픽은 TLS 위에서 동작한다는 전제 위에 코드가 작성되어 있다.
- `src/main/java/com/groute/groute_server/common/config/SecurityConfig.java:85-91` — `ALLOWED_ORIGINS` 에 `https://glit.today`, `https://stg-api.glit.today`, `https://api.glit.today` 만 등록 (운영/스테이징에 대해 HTTP origin 허용 자체가 없음).
- `src/main/resources/application.yaml:226` (stg), `:266` (prod) — `app.swagger.server-url` 이 `https://stg-api.glit.today`, `https://api.glit.today` 로 고정.
- `src/main/resources/application.yaml:143` — `ai.base-url` 기본값 `https://ai.glit.today`.
- 운영 환경의 refresh 쿠키는 `Secure` 속성 강제. 즉 HTTPS 연결이 아니면 쿠키 자체가 전송되지 않으므로 평문 HTTP 경로에선 인증이 동작하지 않는 구조.
- `src/main/java/com/groute/groute_server/auth/service/TokenDeliveryService.java:64-72` — `ResponseCookie ... .secure(true) ... .sameSite("Strict")`.
- `src/main/java/com/groute/groute_server/auth/config/AuthProperties.java:13-17` — prod 프로파일에서 `refreshToken.cookieEnabled=true` (HTTPS 전제, application.yaml:269-271).
- 앱(8080)은 HTTP로만 listen 하지만 TLS 종단은 앞단(AWS ALB/CloudFront 등) 책임으로 분리된 구조. 컨테이너 자체에 인증서를 주입하지 않는다.
- `Dockerfile:5` — `EXPOSE 8080` (443 미노출).
- `docker-compose.yml:4-5` — host 포트 매핑 `"8080:8080"` 만 존재.
- `src/main/resources/application.yaml:91-92` — `server.port: 8080`, `server.ssl.*` 설정 없음.
- 인프라 경계에서 TLS 검증을 가정한 외부 호출들이 모두 https 스킴.
- `src/main/java/com/groute/groute_server/record/adapter/out/ai/AiTaggingClientAdapter.java:55-61` / `src/main/java/com/groute/groute_server/report/adapter/out/ai/AiReportClientAdapter.java:60-72` — AI 서버 호출 `RestClient` baseUrl 이 `https://ai.glit.today` (`ai.base-url` 주입). `JDK HttpClient` 기본 TrustManager로 인증서 검증.
- Swagger UI는 운영에선 비활성화 — TLS 적용 도메인에서 문서가 노출되지 않는다.
- `src/main/resources/application.yaml:258-262` — prod 프로파일에서 `springdoc.swagger-ui.enabled: false`, `springdoc.api-docs.enabled: false`.
### 남은 To-do
- [ ] **HSTS 응답 헤더 강제**: `SecurityConfig` 의 `HttpSecurity.headers(...)` 체인에 `httpStrictTransportSecurity(...)`를 추가해 prod 응답에 `Strict-Transport-Security: max-age=...; includeSubDomains; preload` 가 항상 실리도록 한다.
- 변경 위치: `src/main/java/com/groute/groute_server/common/config/SecurityConfig.java:52-73` (체인 끝에 `.headers(h -> h.httpStrictTransportSecurity(...))` 추가).
- 평가용 액션: prod 응답에서 `curl -I https://api.glit.today/actuator/health | grep -i strict-transport` 으로 헤더 확인.
- [ ] **HTTP → HTTPS 강제 리다이렉트 (앞단 정책 명시)**: 현재 ALB/CloudFront 라우팅 정책이 리포지토리에 기록되어 있지 않다. 인프라 IaC(`infrastructure/`, `terraform/`, 혹은 README 의 "배포 아키텍처" 절)에 ALB listener 80→443 redirect 규칙 / CloudFront `redirect-to-https` viewer protocol policy 명시. (코드 변경 없음, 운영 증빙 보강용)
- [ ] **`server.forward-headers-strategy: framework` 설정**: 현재 yaml 어디에도 forward-headers-strategy 가 없어, ALB가 보낸 `X-Forwarded-Proto: https` 를 Spring이 신뢰하지 않는다. 결과적으로 `HttpServletRequest#isSecure()` 가 ALB 뒤에서 `false` 가 되며, OAuth2 round-trip 시 발행되는 `oauth_env` 쿠키가 prod에서 `Secure` 속성을 받지 못한다.
- 근거: `src/main/java/com/groute/groute_server/auth/service/oauth/OAuth2EnvAwareAuthorizationRequestResolver.java:90` — `.secure(request.isSecure())`.
- 변경 위치: `src/main/resources/application.yaml` 의 stg/prod 프로파일에 `server.forward-headers-strategy: framework` 추가.
- [ ] **OAuth2 redirect-uri 의 스킴 정합성 점검**: 코드의 기본값은 `http://localhost:8080/...` 이지만 SSM 의 `KAKAO_REDIRECT_URI` / `GOOGLE_REDIRECT_URI` / `NAVER_REDIRECT_URI` 가 실제로 `https://` 로 주입되는지 운영 SSM 값 직접 확인. (`aws ssm get-parameters-by-path --path /groute/prod --recursive --with-decryption` 으로 평가 시 캡처)
- 근거: `src/main/resources/application.yaml:49,58,67` — 기본값은 `http://localhost:8080/...`, prod 는 SSM 주입에 전적으로 의존.
- [ ] **HTTPS 미적용 환경에서 쿠키 모드 진입 방지 검증**: `auth.refresh-token.cookie-enabled=true` 인 환경(현재 prod)에서 만약 누군가 HTTP로 ALB 우회 접근을 시도하면 refresh 쿠키가 전송되지 않아 로그인 자체가 실패한다(현재 의도된 동작). 이를 README 보안 절에 명시하고, ALB security group 이 HTTP 인바운드를 차단/리다이렉트하는지 인프라 측에서 한번 더 점검.
- [ ] **(평가 산출물용) HTTPS 인증서 발급/갱신 출처 명시**: ACM/Let's Encrypt 중 무엇으로 발급되는지, 발급자/만료일/자동 갱신 여부를 1페이지로 정리(`docs/operations/tls.md` 등 신규 문서로). 발표 자료 보강에 활용.
---
## 상세 분석
### 1. 시스템 경계와 TLS 종단 지점
이 서비스의 트래픽 구조는 (1) 클라이언트(웹/PWA) ↔ 앞단(ALB/CloudFront) ↔ 컨테이너(Spring Boot 8080) (2) 컨테이너 ↔ 외부 API(카카오/구글/네이버/AI/S3/FCM) 두 갈래다.
(1) 외부 트래픽:
- 컨테이너는 `EXPOSE 8080` (Dockerfile:5), host에서 `8080:8080` 매핑(docker-compose.yml:4-5). `server.ssl.*` 키가 application.yaml 전역에 0건이므로 Spring Boot 자체는 평문 HTTP listener 만 띄운다.
- 따라서 TLS 종단(인증서 보유·핸드셰이크 종료)은 **앱이 아닌 ALB/CloudFront 등 앞단 reverse proxy** 의 책임이다. 이는 AWS 환경에서 가장 일반적이고 권장되는 패턴(ACM cert + ALB listener 443) 이지만, **리포지토리 안에서 인증서 적용을 검증할 자료가 없다는 게 약점**. 평가용으로 IaC/콘솔 캡처/README 보강이 필요.
- 도메인은 `glit.today` 군. `SecurityConfig.java:85-91` 에서 prod/stg origin 을 https 만 화이트리스트.
- 사이드 효과: 만약 ALB 가 80에서 redirect 없이 응답하더라도, Spring CORS preflight 가 `http://api.glit.today` Origin 을 거절한다. 정상 브라우저 요청은 자동으로 차단.
- Swagger UI: prod 에서 끔(`application.yaml:258-262`). API 문서가 HTTPS 도메인에서도 노출되지 않으므로 정보 노출면적 추가 감소.
(2) 백엔드 → 외부 API:
- OAuth provider URL 은 카카오/구글/네이버 모두 https (`application.yaml:74-83`).
- AI 서버: `https://ai.glit.today` 기본값(`application.yaml:143`), `RestClient` 가 `JDK HttpClient` 기본 TrustManager 로 검증(`AiTaggingClientAdapter.java:55-61`, `AiReportClientAdapter.java:60-72`). 평문 옵션 강제 코드 없음.
- S3 Presigned URL: AWS SDK v2 가 기본 https 엔드포인트 사용(`S3Config.java:27-40`).
- FCM: Firebase Admin SDK 기본 https 통신(`FirebaseConfig.java`).
- 즉 아웃바운드는 전 구간이 https.
### 2. 코드가 이미 "TLS 적용을 전제" 하는 지점
다음 코드는 TLS 가 빠지면 그 자체로 기능이 깨지므로, 결과적으로 prod 에서 HTTPS 가 반드시 적용되어 있어야만 정상 동작한다. 평가 답변에서 "왜 HTTPS 가 보장되는가" 의 근거로 활용 가능.
| 위치 | 어떻게 TLS 를 전제하는가 |
|------|-----------------------|
| `TokenDeliveryService.java:64-72` | refresh 쿠키 `.secure(true)` 강제. 평문 HTTP 응답이면 브라우저가 무시 → 로그인 동작 불가. |
| `application.yaml:269-271` (prod) | `auth.refresh-token.cookie-enabled: true`. 위 쿠키 모드 진입 트리거. |
| `SecurityConfig.java:85-91` | CORS `ALLOWED_ORIGINS` 가 https origin 만 허용. 평문 origin 은 preflight 단계에서 거절. |
| `application.yaml:266` (prod) | `server-url: https://api.glit.today` — Swagger/spring doc 의 self-origin 도 https 로 못박음. |
### 3. 코드만 보면 부족한 부분 (남은 To-do 의 근거)
- **HSTS 헤더 미설정**: `SecurityConfig.filterChain` 에 `headers()` DSL 자체가 없음(`SecurityConfig.java:52-73`). Spring Security 기본은 HSTS 를 활성화하지만 `headers(...)` 를 비활성화하지 않은 상태에서만 동작 — 현재는 그래도 디폴트 헤더가 실릴 가능성이 있다. 다만 명시적 설정이 없어 코드 가독성/감사 시 노출 어려움. 명시적으로 `headers(h -> h.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).preload(true).maxAgeInSeconds(31536000)))` 를 권장.
- **forward-headers-strategy 미설정**: `application.yaml` 전체 검색에서 `server.forward-headers-strategy` 가 0건. ALB 뒤에서 `request.isSecure()` 가 false 가 되는 후폭풍을 일으킬 수 있는 지점이 한 곳 있다(`OAuth2EnvAwareAuthorizationRequestResolver.java:90`). 다만 이 쿠키는 임시 env 식별값이라 토큰 자체 보안은 영향 없음. "Secure 미적용 쿠키가 prod 에 존재"는 리뷰어 입장에서 지적 가능 포인트.
- **HTTP→HTTPS 강제 리다이렉트 정책 부재**: 코드/리포지토리 어디에도 명시되지 않음. ALB listener 80 의 redirect 규칙은 인프라 측에서 별도로 설정되어 있을 것으로 추정되나 증빙이 없다. 평가 자료에 인프라 캡처가 필요.
### 4. 평가 답변용 한 문장 요약
> 본 서비스는 모든 외부 진입 도메인(`api.glit.today` / `stg-api.glit.today` / `glit.today`)을 HTTPS 로 고정하고, refresh 토큰을 `Secure; HttpOnly; SameSite=Strict` 쿠키로 발급해 TLS 가 없으면 로그인이 동작하지 않도록 의존성을 강제한다. 백엔드 → 외부(카카오/구글/네이버/AI 서버/S3/FCM) 호출도 전부 `https://` 스킴으로 구성된다. TLS 종단은 AWS ALB가 ACM 인증서로 처리하며, 컨테이너는 8080 평문 listener 로 ALB 뒤에 위치한다.