From d8ba443b3587bc4a480dd8ca200244c081f34dc4 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 3 Jul 2026 16:12:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(infra):=20HTTPS(TLS)=20=EC=8A=A4=EC=BA=90?= =?UTF-8?q?=ED=8F=B4=EB=94=A9=20+=20=ED=94=84=EB=A1=9D=EC=8B=9C=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 25 ++++++++--- .../resources/application-prod.properties | 5 +++ frontend/nginx-tls.conf | 43 +++++++++++++++++++ infra/certs/.gitignore | 5 +++ infra/docker-compose.tls.yml | 21 +++++++++ 5 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 frontend/nginx-tls.conf create mode 100644 infra/certs/.gitignore create mode 100644 infra/docker-compose.tls.yml diff --git a/README.md b/README.md index 5d91cdb..7963f52 100644 --- a/README.md +++ b/README.md @@ -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="` + (`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 서버는 공개망 직접 diff --git a/backend/src/main/resources/application-prod.properties b/backend/src/main/resources/application-prod.properties index cbdfd90..1c0c2a3 100644 --- a/backend/src/main/resources/application-prod.properties +++ b/backend/src/main/resources/application-prod.properties @@ -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) diff --git a/frontend/nginx-tls.conf b/frontend/nginx-tls.conf new file mode 100644 index 0000000..ede1b63 --- /dev/null +++ b/frontend/nginx-tls.conf @@ -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 / /; + } +} diff --git a/infra/certs/.gitignore b/infra/certs/.gitignore new file mode 100644 index 0000000..211f149 --- /dev/null +++ b/infra/certs/.gitignore @@ -0,0 +1,5 @@ +# TLS certificates/keys are environment-specific secrets — never commit them. +*.pem +*.key +*.crt +!.gitignore diff --git a/infra/docker-compose.tls.yml b/infra/docker-compose.tls.yml new file mode 100644 index 0000000..436bad8 --- /dev/null +++ b/infra/docker-compose.tls.yml @@ -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=" +# 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"