fix #547: secure VM deployment behind HTTPS proxy
This commit is contained in:
125
docs/design/547-vm-https-hardening/README.md
Normal file
125
docs/design/547-vm-https-hardening/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# 설계서: 외부 VM HTTPS 및 직접 포트 차단 (#547)
|
||||
|
||||
> **상태**: Approved
|
||||
> **작성**: [AI] Architect · **최종수정**: 2026-06-28
|
||||
> **추적성** — Redmine: #547 · 관련 ADR: 없음
|
||||
> · 구현 파일: `src/main/resources/application.yml`, `SecurityConfig.java`, `scripts/deploy-backoffice-vm.sh`, `scripts/configure-backoffice-https-vm.sh`, `deploy/caddy/Caddyfile.template`
|
||||
> · 테스트: `TransportSecurityTest.java`, `scripts/test-backoffice-https-config.sh`
|
||||
|
||||
## 1. 목적 (Why)
|
||||
|
||||
외부 VM에서 관리자 자격증명과 세션 쿠키가 평문 HTTP로 전송되고 애플리케이션 포트 `8082`가 직접 노출되는 경로를 제거한다.
|
||||
|
||||
## 2. 범위 (Scope)
|
||||
|
||||
- **포함**: 공개 DNS 이름 또는 통제 가능한 public IPv4 기반 HTTPS, HTTP→HTTPS 전환, Caddy TLS termination과 인증서 자동 갱신, 애플리케이션 루프백 바인딩, forwarded header 처리, Secure/HttpOnly/SameSite 세션 쿠키, HSTS, 배포·검증·롤백 절차.
|
||||
- **제외**: OCI NSG와 VM firewalld의 실제 변경, DNS 레코드 생성, Caddy 패키지 설치, 기본 `admin/admin` 제거(#548), CSP/CDN 공급망 강화(#549).
|
||||
|
||||
## 3. 인수조건 (Acceptance Criteria)
|
||||
|
||||
- [ ] 외부 HTTP 요청은 HTTPS로 전환되고 로그인 폼은 HTTPS에서만 제출된다.
|
||||
- [ ] 애플리케이션은 VM의 `127.0.0.1:8082`에만 바인딩되어 외부 직접 접속이 실패한다.
|
||||
- [ ] HTTPS 로그인 응답의 `JSESSIONID`에 `Secure`, `HttpOnly`, `SameSite=Lax`가 있다.
|
||||
- [ ] HTTPS 응답에 1년 HSTS가 있고, 전달된 scheme/host가 redirect 생성에 반영된다.
|
||||
- [ ] 공개 프록시는 클라이언트가 보낸 `X-Forwarded-*`를 신뢰하지 않고 직접 다시 설정한다.
|
||||
- [ ] 인증서 갱신·기동·헬스체크·롤백 절차가 자동화 스크립트와 런북에 있다.
|
||||
|
||||
## 4. 컨텍스트 & 제약
|
||||
|
||||
- Caddy automatic HTTPS는 외부 80/443 접근과 지속 가능한 인증서 저장소를 전제로 인증서를 발급·갱신하고 HTTP를 HTTPS로 전환한다.
|
||||
- Let’s Encrypt public IPv4 인증서는 2026년부터 정식 지원되며 `shortlived` ACME profile과 약 6일 수명을 사용한다. Caddy가 profile과 갱신을 지속적으로 관리한다.
|
||||
- FQDN이 있으면 일반 공개 인증서를 우선하고, DNS가 준비되지 않은 단일 VM은 통제 중인 고정 public IPv4 인증서를 사용할 수 있다. Caddy 로컬 CA 인증서는 운영에 사용하지 않는다.
|
||||
- 애플리케이션의 forwarded header 처리는 연결 원본 CIDR을 자체 판별하지 않는다. 따라서 `127.0.0.1` 바인딩을 보안 경계로 삼아 같은 VM의 Caddy만 접근시킨다.
|
||||
- Caddy는 인터넷에 직접 연결되는 첫 프록시다. CDN/로드밸런서를 추가할 때만 명시적인 `trusted_proxies` CIDR 검토가 필요하다.
|
||||
- 방화벽/NSG 변경은 서비스 영향과 잠금 위험이 있어 스크립트가 자동 수행하지 않는다.
|
||||
|
||||
## 5. 아키텍처 개요
|
||||
|
||||
```text
|
||||
Internet client
|
||||
├─ HTTP :80 ───────> Caddy ── 308 HTTPS redirect
|
||||
└─ HTTPS :443 ─TLS─> Caddy ── X-Forwarded-* 재생성
|
||||
│
|
||||
└─ HTTP 127.0.0.1:8082
|
||||
Spring Boot
|
||||
├─ require-https
|
||||
├─ HSTS
|
||||
└─ Secure/HttpOnly/SameSite cookie
|
||||
|
||||
Internet client ── HTTP :8082 ──X (loopback bind + firewalld/NSG deny)
|
||||
```
|
||||
|
||||
- `deploy/caddy/Caddyfile.template`: TLS/redirect/HSTS/reverse proxy의 선언적 설정.
|
||||
- `scripts/configure-backoffice-https-vm.sh`: 입력 검증, 원격 설정 검증·백업·적용·확인.
|
||||
- `scripts/deploy-backoffice-vm.sh`: 앱 배포 시 운영 보안 환경값을 강제하고 루프백 헬스체크.
|
||||
- Spring 설정: proxy가 전달한 HTTPS scheme을 인식하고 직접 HTTP 요청을 거부한다.
|
||||
- 순수 경계 검증(호스트명, 이메일, 포트, 템플릿 렌더링)은 로컬 dry-run 테스트로 검증하고, SSH/systemd/ACME는 명시적 운영 단계에서 수행한다.
|
||||
|
||||
## 6. 설정 모델
|
||||
|
||||
| 설정 | 운영값 | 검증 |
|
||||
|---|---|---|
|
||||
| `BACKOFFICE_BIND_ADDRESS` | `127.0.0.1` | HTTPS 배포 스크립트가 고정 |
|
||||
| `BACKOFFICE_FORWARD_HEADERS_STRATEGY` | `framework` | 배포 환경 override |
|
||||
| `BACKOFFICE_REQUIRE_HTTPS` | `true` | 배포 환경 override |
|
||||
| `BACKOFFICE_SESSION_COOKIE_SECURE` | `true` | 배포 환경 override |
|
||||
| public host | 소유한 FQDN 또는 고정 public IPv4 | FQDN/IP 문법과 expected address 일치 검증 |
|
||||
| TLS email | ACME 운영 이메일 | 공백/개행/비정상 형식 거부 |
|
||||
| app port | `8082` | 1–65535 숫자 |
|
||||
|
||||
로컬 개발은 기존 HTTP 흐름을 보존하기 위해 HTTPS 강제와 Secure cookie의 기본값을 `false`로 둔다. VM 배포 산출물에서만 안전한 운영값으로 덮어쓴다.
|
||||
|
||||
## 7. 함수/스크립트 명세
|
||||
|
||||
| 함수/스크립트 | 책임 | 입력 | 출력 | 실패 |
|
||||
|---|---|---|---|---|
|
||||
| `securityFilterChain` | 인증, HTTPS channel, HSTS 구성 | 보안 설정 | servlet filter chain | 구성 오류 시 기동 실패 |
|
||||
| `deploy-backoffice-vm.sh` | jar/env/wallet 배포와 내부 헬스체크 | SSH 대상, 포트 | 루프백 앱 | SSH/build/start 실패 |
|
||||
| `configure-backoffice-https-vm.sh` | Caddy 설정 적용·검증 | FQDN, TLS email, SSH 대상 | HTTPS endpoint | DNS/Caddy/sudo/TLS 검증 실패 |
|
||||
| `test-backoffice-https-config.sh` | 입력 거부와 Caddy 템플릿 검증 | 로컬 저장소 | PASS/FAIL | assertion 실패 |
|
||||
|
||||
## 8. 적용 흐름
|
||||
|
||||
1. 소유한 FQDN의 A/AAAA 레코드를 VM에 연결하거나, 고정 public IPv4를 endpoint로 확정한다.
|
||||
2. 개별 승인으로 NSG/firewalld에서 80/443을 열고 8082 ingress를 제거한다.
|
||||
3. VM에 공식 Caddy 패키지와 systemd service가 준비됐는지 확인한다.
|
||||
4. 앱을 재배포해 `127.0.0.1:8082`, forwarded headers, HTTPS 강제, Secure cookie를 활성화한다.
|
||||
5. Caddy 설정을 문법 검증한 후 기존 설정을 timestamp backup하고 원자적으로 설치한다.
|
||||
6. HTTP redirect, TLS 신뢰, HSTS, cookie attributes, `:8082` 차단을 외부에서 검증한다.
|
||||
|
||||
## 9. 엣지케이스 & 에러 처리
|
||||
|
||||
- FQDN DNS가 아직 VM을 가리키지 않으면 Caddy 설정을 적용하지 않는다.
|
||||
- IPv4 endpoint는 expected address와 같아야 하며 `shortlived` profile 없는 설정을 허용하지 않는다.
|
||||
- wildcard hostname은 이 단일 VM 스크립트에서 거부한다.
|
||||
- Caddy 설정 검증 또는 reload가 실패하면 직전 Caddyfile을 복원하고 reload한다.
|
||||
- 외부 HTTPS 검증이 실패해도 앱의 루프백 프로세스는 유지하며 로그와 복구 명령을 출력한다.
|
||||
- Caddy 장애 시 `8082`를 공개하는 방식으로 우회하지 않는다. 직전 Caddyfile 복원 또는 앱/프록시 동시 롤백만 허용한다.
|
||||
|
||||
## 10. 테스트 계획
|
||||
|
||||
- Maven 통합 테스트:
|
||||
- 직접 HTTP 요청이 HTTPS로 redirect되는지 확인.
|
||||
- `X-Forwarded-Proto=https`, `X-Forwarded-Host`를 적용한 요청이 정상 처리되고 HSTS가 있는지 확인.
|
||||
- 운영 환경변수가 Secure/HttpOnly/SameSite cookie 설정에 결합되는지 확인.
|
||||
- 셸 테스트:
|
||||
- FQDN/email/port 입력 검증.
|
||||
- 렌더링된 Caddyfile에 public host, HSTS, 루프백 upstream이 있는지 확인.
|
||||
- 배포 dry-run이 루프백/HTTPS 운영 override를 표시하는지 확인.
|
||||
- 운영 검증:
|
||||
- `curl -I http://<fqdn>/login`
|
||||
- `curl -I https://<fqdn>/login`
|
||||
- HTTPS GET의 `Set-Cookie`
|
||||
- 외부 `curl http://<fqdn>:8082/login` 실패
|
||||
- `systemctl is-active caddy`와 Caddy journal의 갱신 오류 확인.
|
||||
|
||||
## 11. 리스크 & 대안 검토
|
||||
|
||||
- Caddy를 선택한 이유는 HTTP redirect, ACME 발급/갱신, reverse proxy를 하나의 짧은 선언으로 운영할 수 있기 때문이다.
|
||||
- Nginx+Certbot은 가능하지만 인증서 갱신 hook과 설정 검증 경로가 분리되어 이 단일 VM PoC의 운영 표면이 더 넓다.
|
||||
- Spring Boot 직접 TLS는 애플리케이션 재기동과 인증서 교체가 결합되고 80→443 처리 및 권한 분리가 불리해 제외한다.
|
||||
|
||||
## 12. 미해결 질문 (배포 전 필수 입력)
|
||||
|
||||
- 사용할 공개 FQDN 또는 `130.162.134.59` 고정 IP와 ACME 알림 이메일.
|
||||
- DNS 변경 완료 여부와 80/443 NSG/firewalld 변경 승인.
|
||||
109
docs/runbooks/547-vm-https-operations.md
Normal file
109
docs/runbooks/547-vm-https-operations.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Redmine #547 - 외부 VM HTTPS 운영 런북
|
||||
|
||||
## 운영 구조
|
||||
|
||||
- 공개 endpoint: `https://<소유 FQDN>` 또는 `https://<고정 public IPv4>`
|
||||
- Caddy: VM의 80/443에서 TLS termination, HTTP redirect, HSTS, 인증서 자동 발급·갱신
|
||||
- Spring Boot: `127.0.0.1:8082`에서만 수신
|
||||
- 외부 `8082/tcp`: OCI NSG와 VM firewalld 모두 deny
|
||||
|
||||
FQDN을 쓰면 A 레코드는 VM public IP를 가리켜야 한다. DNS가 없는 고정 public IPv4는 Let’s Encrypt `shortlived` profile 기반 약 6일 인증서를 사용하며 Caddy 자동 갱신이 정상인지 반드시 모니터링한다. 임시 공개 DNS 서비스와 Caddy 로컬 CA 인증서는 운영 endpoint로 사용하지 않는다.
|
||||
|
||||
## 최초 준비
|
||||
|
||||
아래 작업은 시스템 패키지와 방화벽을 바꾸므로 개별 승인을 받은 뒤 수행한다.
|
||||
|
||||
1. 소유 FQDN의 A 레코드를 VM public IP로 설정하거나 고정 public IPv4 사용을 확정한다.
|
||||
2. OCI NSG와 VM firewalld에서 80/443 ingress를 허용한다.
|
||||
3. 기존 8082 ingress를 OCI NSG와 firewalld에서 제거한다.
|
||||
4. Oracle Linux/RHEL 계열 VM에 공식 Caddy 패키지를 설치한다.
|
||||
|
||||
```bash
|
||||
sudo dnf install -y dnf-plugins-core
|
||||
sudo dnf copr enable -y @caddy/caddy
|
||||
sudo dnf install -y caddy
|
||||
```
|
||||
|
||||
공식 패키지는 `caddy.service`와 `/etc/caddy/Caddyfile`을 제공한다. 설정 스크립트가 service enable/start를 처리한다.
|
||||
|
||||
## 적용
|
||||
|
||||
먼저 로컬 검증과 앱 배포를 수행한다.
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
scripts/test-backoffice-https-config.sh
|
||||
scripts/deploy-backoffice-vm.sh --host hermes
|
||||
```
|
||||
|
||||
HTTPS 설정을 dry-run으로 확인한 다음 적용한다.
|
||||
|
||||
```bash
|
||||
scripts/configure-backoffice-https-vm.sh \
|
||||
--host hermes \
|
||||
--public-host admin.example.com \
|
||||
--tls-email ops@example.com \
|
||||
--expected-address 130.162.134.59 \
|
||||
--dry-run
|
||||
|
||||
scripts/configure-backoffice-https-vm.sh \
|
||||
--host hermes \
|
||||
--public-host admin.example.com \
|
||||
--tls-email ops@example.com \
|
||||
--expected-address 130.162.134.59
|
||||
```
|
||||
|
||||
실제 endpoint, 이메일, 주소로 바꿔 실행한다. FQDN 대신 IP를 쓰는 현재 hermes 예시는 `--public-host 130.162.134.59 --expected-address 130.162.134.59`다. 스크립트는 DNS/IP 일치, Caddy/sudo, Caddyfile 문법, service 상태를 확인하고 기존 설정을 `~/apps/vpd-backoffice/caddy-backups`에 저장한다.
|
||||
|
||||
## 반복 검증
|
||||
|
||||
설정을 바꾸지 않고 외부 검증만 다시 수행할 수 있다.
|
||||
|
||||
```bash
|
||||
scripts/configure-backoffice-https-vm.sh \
|
||||
--host hermes \
|
||||
--public-host admin.example.com \
|
||||
--tls-email ops@example.com \
|
||||
--expected-address 130.162.134.59 \
|
||||
--verify-only
|
||||
```
|
||||
|
||||
검증 항목:
|
||||
|
||||
- HTTP `/login`이 동일 host의 HTTPS로 전환됨
|
||||
- HTTPS 인증서가 공개 신뢰됨
|
||||
- HSTS 1년
|
||||
- `JSESSIONID`의 Secure/HttpOnly/SameSite=Lax
|
||||
- 외부 `:8082` 직접 연결 실패
|
||||
|
||||
## 인증서 갱신과 모니터링
|
||||
|
||||
Caddy는 공개 DNS 이름의 인증서를 자동 갱신하며, 공식 systemd service의 인증서 상태는 `/var/lib/caddy/.local/share/caddy`에 유지된다. 별도 cron이나 certbot hook을 추가하지 않는다.
|
||||
|
||||
```bash
|
||||
ssh hermes 'systemctl is-active caddy && systemctl is-enabled caddy'
|
||||
ssh hermes 'sudo journalctl -u caddy --since "24 hours ago" --no-pager'
|
||||
ssh hermes 'sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile'
|
||||
```
|
||||
|
||||
ACME 오류, 인증서 만료 경고, 반복 reload 실패를 알림 대상으로 삼는다. VM 백업에서 Caddy data directory와 앱의 Caddyfile backup을 함께 보존한다.
|
||||
|
||||
## 장애와 롤백
|
||||
|
||||
1. 앱이 살아 있는지 VM 내부에서 확인한다.
|
||||
|
||||
```bash
|
||||
ssh hermes 'curl -sS -H "X-Forwarded-Proto: https" -H "X-Forwarded-Host: admin.example.com" -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8082/login'
|
||||
```
|
||||
|
||||
2. Caddy 로그와 설정을 확인한다.
|
||||
3. 설정 변경 직후 장애라면 가장 최근 backup을 복원한다.
|
||||
|
||||
```bash
|
||||
ssh hermes 'ls -1t ~/apps/vpd-backoffice/caddy-backups/Caddyfile.* | head -n 3'
|
||||
ssh hermes 'sudo install -o root -g root -m 644 ~/apps/vpd-backoffice/caddy-backups/Caddyfile.<timestamp> /etc/caddy/Caddyfile && sudo systemctl reload caddy'
|
||||
```
|
||||
|
||||
4. 앱 jar 롤백이 필요하면 직전 승인된 artifact를 배포하고 앱과 Caddy를 모두 재검증한다.
|
||||
|
||||
장애 우회를 위해 `8082`를 다시 공개하지 않는다. 서비스 중단이나 방화벽/NSG 롤백은 개별 승인을 받는다.
|
||||
Reference in New Issue
Block a user