feat(infra): HTTPS(TLS) 스캐폴딩 + 프록시 헤더 처리

- Spring prod: server.forward-headers-strategy=framework — nginx의 X-Forwarded-Proto를
  신뢰해 프록시 뒤에서도 request.isSecure()=true → Secure 쿠키·https 리다이렉트 정상화.
- frontend/nginx-tls.conf: 443 TLS 종료 + 80→443 리다이렉트(+ /api 프록시).
- infra/docker-compose.tls.yml: base와 함께 쓰는 HTTPS 오버레이(443 매핑·인증서 볼륨·
  ACS_PUBLIC_BASE_URL/ACS_COOKIE_SECURE). infra/certs/.gitignore로 인증서·키 커밋 제외.
- README: HTTPS 적용 절차(인증서 준비→.env→compose 오버레이) 갱신.

검증: forward-headers=framework + cookie.secure=true로 기동 후 X-Forwarded-Proto=https 유무 대조 —
헤더 있으면 XSRF-TOKEN·JSESSIONID 모두 Secure, 없으면 XSRF Secure 미부여(프록시 프로토콜 연동 확인).
자체서명 인증서 생성 확인. 전체 컨테이너 TLS e2e는 Docker 데몬 기동 시 별도 검증 필요.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
unknown
2026-07-03 16:12:14 +09:00
parent 17d6036bca
commit d8ba443b35
5 changed files with 93 additions and 6 deletions

View File

@@ -80,13 +80,26 @@ docker compose logs -f app # 기동/마이그레이션 로그
1. 서버에서 메시지 API 도달 확인: `nc -vz 210.104.132.59 8000`
2. `ACS_SMS_PROVIDER=hanbank`, `ACS_PUBLIC_BASE_URL`=외부 접속 URL 로 설정 후 재기동
### HTTPS / 카메라 스캔 (선택, 접속 방식에 따라 필요)
- 출입콘솔의 **웹캠 QR 스캔**은 secure context(HTTPS 또는 localhost)에서만 동작한다.
서버를 `http://내부IP`로 접속하면 카메라가 차단된다.
### HTTPS 적용 (TLS 종료는 nginx가 담당)
브라우저 → **nginx(web, TLS 종료)** → 내부 http → Spring(app) 구조. 앱은 `server.forward-headers-strategy=framework`
(prod에 반영됨)로 nginx의 `X-Forwarded-Proto`를 신뢰해 Secure 쿠키·https 리다이렉트를 처리한다.
TLS 오버레이가 준비돼 있다 — 인증서만 넣으면 된다:
1. **인증서 준비**`infra/certs/{fullchain.pem,privkey.pem}` (사내 CA 발급 / 공인 CA / 테스트용 자체서명).
자체서명 예: `openssl req -x509 -newkey rsa:2048 -nodes -days 825 -keyout infra/certs/privkey.pem -out infra/certs/fullchain.pem -subj "/CN=<host>"`
(`infra/certs/*.pem``.gitignore`로 커밋 제외.)
2. **`.env`**: `ACS_PUBLIC_BASE_URL=https://<도메인>`, `ACS_COOKIE_SECURE=true`.
3. **기동** (base + TLS 오버레이):
```bash
cd infra
docker compose -f docker-compose.yml -f docker-compose.tls.yml up -d --build
```
→ nginx가 `443` TLS 종료 + `80→443` 리다이렉트. `frontend/nginx-tls.conf` 사용.
`ACS_COOKIE_SECURE=true`는 위처럼 **실제 HTTPS로 서비스될 때만** 켠다(평문 http에서 켜면 쿠키 미전송 → 로그인 불가).
- 출입콘솔의 **웹캠 QR 스캔**은 secure context(HTTPS 또는 localhost)에서만 동작 → HTTPS면 `http://내부IP`에서도 카메라 사용 가능.
- 방문자 공개 링크(`/pass/{token}`)도 HTTPS 도메인이면 휴대폰에서 안전하게 열린다.
- 적용하려면: 사내 도메인/인증서 확보 → `frontend/nginx.conf`에 443 TLS server 블록 추가,
`docker-compose.yml` web 서비스에 `"443:443"` 매핑 + 인증서 볼륨 마운트,
`ACS_PUBLIC_BASE_URL``https://...`로 설정. (도메인/인증서 확정 후 진행)
### 사내망 빌드 참고
`docker compose build`는 컨테이너 안에서 npm/maven 의존성을 받는다. dev 서버는 공개망 직접

View File

@@ -1,6 +1,11 @@
spring.application.name=acs
server.port=8080
# ===== Reverse proxy (nginx terminates TLS) =====
# Honor X-Forwarded-Proto/For from the nginx front so request.isSecure() is true behind
# TLS termination — makes the CSRF XSRF-TOKEN cookie Secure and redirects use https.
server.forward-headers-strategy=framework
# ===== Session cookie hardening =====
# SameSite=Lax complements the CSRF token defense.
# Secure=true means the cookie is only sent over HTTPS — enable it (ACS_COOKIE_SECURE=true)

43
frontend/nginx-tls.conf Normal file
View File

@@ -0,0 +1,43 @@
# HTTPS variant of nginx.conf — used by the docker-compose.tls.yml overlay.
# nginx terminates TLS and reverse-proxies /api to the backend over the internal network.
# Mount a certificate at /etc/nginx/certs/{fullchain.pem,privkey.pem}.
# Redirect all plain HTTP to HTTPS.
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name _;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /usr/share/nginx/html;
index index.html;
# SPA client-side routing: fall back to index.html
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to the backend service (session cookie preserved).
# X-Forwarded-Proto=https lets the app (server.forward-headers-strategy=framework)
# know the original request was secure → Secure cookies + https redirects.
location /api/ {
proxy_pass http://app:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cookie_path / /;
}
}

5
infra/certs/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# TLS certificates/keys are environment-specific secrets — never commit them.
*.pem
*.key
*.crt
!.gitignore

View File

@@ -0,0 +1,21 @@
# HTTPS overlay. Use together with the base compose file:
# docker compose -f docker-compose.yml -f docker-compose.tls.yml up -d --build
#
# Requires a TLS certificate under ./certs/{fullchain.pem,privkey.pem}.
# For a quick internal/test cert (self-signed):
# openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
# -keyout certs/privkey.pem -out certs/fullchain.pem -subj "/CN=<your-host>"
# For production, drop in the cert issued for your internal domain (corporate CA / public CA).
services:
web:
ports:
- "${WEB_TLS_PORT:-443}:443" # base file already maps WEB_PORT:80 (used for the redirect)
volumes:
- ./certs:/etc/nginx/certs:ro
- ../frontend/nginx-tls.conf:/etc/nginx/conf.d/default.conf:ro
app:
environment:
# The public link in the pass email/SMS + the secure-cookie switch.
ACS_PUBLIC_BASE_URL: ${ACS_PUBLIC_BASE_URL:-https://localhost}
ACS_COOKIE_SECURE: "true"