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"