POSA_LEAKSMS/CHANGELOG.md
cw2007 73e49fd528 feat: RPi4 누수+온습도 2-모듈 단일 서버 통합 + 보안 하드닝
2번 모듈 신규(raspberry_pi4_sht30/): SHT30 온습도 → cafe24, raw-body X-Signature HMAC, 폐쇄망 오프라인 설치(vendor/install), systemd, CRC/포맷 자가점검. firmware sht30_fw wire 포맷과 byte-parity 검증.

1번 모듈 통합(raspberry_pi4/): 누수+카메라를 동일 서버로. 레거시 sha256(key+json) body-sig → X-Signature HMAC 전환, 사진 canonical=device_id|sensor_id|log_id|timestamp.

서버 통합(php/): api/sensor_data.php 누수 이벤트 수신, api/photo_upload.php 신규(HMAC·finfo·서버생성 파일명·nonce 멱등·sensor 결속), photo.php 인증 사진서빙, dashboard.php 누수 패널+사진, sms_send.php 누수 SMS, config.php 태그/요약.

DB: sql/migration_add_leak.sql(가산·멱등), schema_sht30.sql 통합 스키마.

보안 하드닝(외부 펜테스트 대비, 확정 CRITICAL/HIGH 0): cron_heartbeat api_key fail-closed, setup_mfa 부트스트랩 토큰 post-enrollment 잠금, var/ PHP 실행차단, HSTS.

문서/테스트: SYSTEM_OVERVIEW.md, security_hardening_test.php 갱신. 참고: 사용자 요청으로 세션 외 기존 미커밋 변경도 함께 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:21:11 +09:00

287 lines
23 KiB
Markdown

# 변경 이력
## Unreleased - 기능 추가 (고가치 M: 신뢰성·관측성)
경보가 조용히 죽지 않도록 보장 + 그것을 검증하는 신뢰성·관측성 묶음. 모두 cron/서버/
CLI 기반(새 상호작용 UI 없음), graceful degrade. (#5 임계·수신자 화면편집은 core config
로드를 DB 의존으로 바꿔 신뢰성 회귀 위험이 있어 별도 설계로 보류.)
### M#1 수집 거부 사유 카운터 (관측성)
- `config.php:record_ingest_rejection()/read_ingest_rejections()`(파일 flock 기반, DB 무관
→ 위조요청 폭주에도 안전). `api/sensor_data.php` 의 모든 거부 분기(403 서명/400 시각/
413 본문/503 키/필드/sensor_id)를 사유별 일자 집계하고, 대시보드 상단에 사유별
건수(서명불일치→키/펌웨어, 시각오차→SNTP/RTC 등 한국어 라벨)로 노출한다. "왜 데이터가
안 들어오는가"를 서버 로그 없이 분별.
### M#2 발송 실패 가시화 + 오프라인 재시도 (신뢰성)
- `dashboard.php`: 최근 1시간 내 경보/오프라인/에스컬레이션 SMS 가 실패하면 '경보 SMS
발송 실패 — 미수신 가능' 배너.
- `cron_heartbeat.php`: 일회성 오프라인 SMS 발송이 실패하면 `offline_alerted` 를 0 으로
되돌려 다음 주기에 재시도한다(`SMS_RETRY_OFFLINE`). 게이트웨이 일시오류로 장비 다운을
영영 못 알리던 갭을 닫음.
### M#3 미복귀 경보 에스컬레이션 (신뢰성, opt-in)
- `sms_send.php:maybe_escalate_alerts()`: 임계 경보가 `ESCALATE_AFTER_SEC`(기본 30분) 이상
미복귀로 지속되면 2차(야간) 수신자 `SMS_RECIPIENTS_TIER2` 에게 `[에스컬레이션]` 1회
통지(에피소드당 1회 — 최신 경보-상태가 경보일 때만). TIER2 가 비면 비활성. cron 호출.
- 대화형 ack 버튼 UI 는 보류(별도 POST 표면 — 후속).
### M#4 센서 고착(live-but-stuck) 검출 (자가신뢰성)
- `sms_send.php:maybe_detect_stuck_sensors()`: 최근 `STUCK_DETECT_SAMPLES`(6) 측정의 온도·
습도 표준편차가 둘 다 `STUCK_DETECT_EPSILON`(0.01, 분해능 이하) 미만이면 소자 고장(같은
값 반복)을 의심해 `[센서고착]` 1회(쿨다운). 하트비트(수신 끊김)로는 못 잡는 무성실패를
보완 — 단일 센서가 거짓말하면 감시 전체가 무력화되므로 핵심.
### M#6 카오스 드릴 (검증)
- `SMS_FORCE_FAIL` 게이트(`send_sms_cafe24` 가 실제 전송 없이 실패 반환)와
`tests/chaos_drill.php`(CLI) — 위 실패-의존 기능이 동작하는지 손시연 없이 검증.
라이브 종단 드릴 절차는 파일 헤더에 문서화.
### 테스트
- `tests/threshold_sms_test.php`: `stddev_pop` 단위 테스트(고착 판정).
- `tests/security_hardening_test.php`: M#1~#6 소스 회귀 고정.
- `run_tests.sh`: 드릴 포함.
### 보류(별도 설계 필요)
- 경보 ack 버튼 UI(대화형 POST), 임계·수신자 화면편집(#5, core config DB-override —
DB 장애 시 config 로드 실패 → 사이트 다운 위험), 풀 카오스 하네스(prod DB 주입).
## Unreleased - 기능 추가 (①시계동기 ②경보 생존성/결로/개인정보)
전수 아이디에이션에서 도출한 우선순위 1~2(①+②)를 구현했다. 외부 의존 0, 대부분
기존 파이프라인 재사용.
### ① 폐쇄망 시계동기 — 인터넷 NTP 의존 제거 (잠재 전면장애 해소)
- **`firmware/common/app_config.h`: `APP_SNTP_SERVER` 기본값을 `pool.ntp.org`(인터넷)에서
LAN NTP 자리표시자(`192.0.2.123`, RFC5737)로 교체**하고 폐쇄망 필수 설정임을 명시했다.
진짜 폐쇄망에서 인터넷 NTP 는 도달 불가 → SNTP 실패 → RTC 미설정 → `timestamp=0` →
TLS 차단 + 서버 신선도 위반 전량거부 = **보고 전면장애**였다(제약 위반 상태 해소).
- `api/sensor_data.php`: 200 응답에 `server_now`(서버 Unix 시각)를 추가했다. 펌웨어가
RTC 드리프트를 서버시각으로 미세보정(LSI 드리프트/간헐 NTP 대비)하는 **후속 펌웨어
작업의 입력**이다(서버측 선반영; 보안 영향 없음).
- TODO(운영): 배포 전 `APP_SNTP_SERVER` 를 실제 LAN NTP IP 로 설정. 최초 시각 확보는
LAN NTP(또는 배터리 RTC)가 필요하며, `server_now` 소비(RTC 보정)는 펌웨어 후속 과제.
### ②a SMS 잔액 사전경보
- `sms_send.php`: Cafe24 응답의 잔여 건수를 파싱(`send_sms_cafe24` 가 `remaining` 반환)하고,
임계(`SMS_BALANCE_WARN_THRESHOLD`, 기본 30) 이하이면 쿨다운(`SMS_BALANCE_WARN_COOLDOWN_SEC`)
적용 `[SMS 잔액부족]` 경고를 1회 발송한다(`maybe_warn_sms_balance`). 추가 외부호출 0.
- `config.php:latest_sms_balance()` + `ops_checks.php` 에 'SMS 잔여 건수' 점검 추가.
### ②b 일일 헬스(생존신호) SMS — 데드맨
- `sms_send.php:maybe_send_daily_health_sms()`: 하루 1통 `[정기점검]` 요약(정상 센서 수/
최근 수신/24h 경보/SMS 잔여)을 발송한다. `HEALTH_SMS_HOUR`(기본 9시) 이후 첫 호출에서,
날짜 dedup + 네임드 락으로 중복 없이 1회. `cron_heartbeat.php` 가 호출. '무소식=정상'
과 '무소식=경보경로 전체고장'의 모호성을 제거(관리자는 "안 오면 점검"만 기억).
### ②c 이슬점/결로 경보
- `sms_send.php`: Magnus 이슬점(`dew_point_c`)·결로마진(`condensation_margin_c`)을 추가하고,
`(현재온도−이슬점) ≤ METRIC_DEWPOINT_MARGIN_C`(기본 3℃)이면 새 종류 `condensation_risk`/
`[결로주의]` 경보를 발화한다. `evaluate_metric_thresholds`(말미 추가)·`metric_is_recovered`
(결로 해소 히스테리시스 포함)·`send_metric_alert_sms`(라벨)·`sensor_data.php`(표시값=결로마진)
에 통합. 온도·습도가 각각 정상이어도 잡히는 결합 위험(차가운 랙면 결로)을 단일 SHT30
산술만으로 포착 — 누수감지기 후신인 이 시스템의 본래 목적에 부합.
### ②d 수신번호 개인정보 마스킹
- `config.php:mask_phone()`(010-****-1234)을 `dashboard.php`·`setup_wizard.php` 의 수신번호
표시에 적용했다. 발송 기록(`sms_log`)은 보존하되 화면 표시만 최소화(개인정보 최소표시).
### 테스트
- `tests/threshold_sms_test.php`: 이슬점/결로마진/해소·`mask_phone` 경계 테스트(기존
임계 벡터는 결로 미발화 확인 — 회귀 없음).
- `tests/security_hardening_test.php`: ①+② 소스 회귀 고정(SNTP 비-인터넷, server_now,
잔액/헬스/결로/마스킹 상수·함수·호출).
## Unreleased - 개선 구현 (다중 에이전트 전수 점검 반영)
전체 서브시스템(펌웨어/PHP/DB/문서/빌드)을 다중 에이전트로 매핑·교차검증하여 도출한
개선 목록을 구현했다. **호환성 유지(가산/멱등)** 를 원칙으로 했으며, 새 DB 테이블은
없어도 동작하도록 graceful degrade 한다.
### 보안 / 신뢰성 (서버 PHP)
- **재전송(replay) 멱등 방어 추가.** `api/sensor_data.php` 가 인증·신선도를 통과한 요청
서명(X-Signature)을 `ingest_nonce` 테이블에 원자적으로 기록한다. 시계오차 창
(`INGEST_MAX_CLOCK_SKEW_SEC`) 안에서 같은 서명 본문이 다시 오면(펌웨어 재시도/캡처
재생) 중복 저장·중복 SMS 없이 **멱등 200** 으로 응답한다. 저장 실패 시 nonce 를
되돌려 재시도가 재처리되게 한다(유실 방지). 테이블이 없으면 자동 비활성
(`INGEST_NONCE_ENABLED`).
- **임계 경보 SMS 중복 발송 경합 제거.** `sms_send.php:send_metric_alert_sms()` 의
쿨다운 검사→발송→기록 구간을 `(센서,종류)` 단위 MySQL 네임드 락(`GET_LOCK`)으로
직렬화한다. 락 미지원 환경에서는 종전 동작으로 폴백한다(가용성 우선).
- **SMS 길이 캡.** Cafe24 단문('S', EUC-KR ≤90B) 한도를 넘는 메시지를 발송 전
멀티바이트 경계 보존하며 자른다(`sms_truncate_euckr`, `SMS_MAX_EUCKR_BYTES`). 위치/
센서명이 길 때 게이트웨이의 무성 절단/거부를 막는다.
- **경보 태그 단일 출처화.** `[고온경보]` 등 상태 태그를 `config.php` 의 `SMS_TAG_*`
상수와 `sms_summary_select_sql()` 헬퍼로 일원화하고, `sms_send/dashboard/monthly_report`
의 하드코딩 LIKE 리터럴을 제거했다. 태그를 한 곳만 바꿔도 쿨다운/복구/대시보드/월간
보고서가 일관 유지된다(문자열 어긋남 무성 장애 방지).
- **세션 절대 수명 캡 추가 + 모든 인증 페이지로 확대.** 유휴 타임아웃(`SESSION_TIMEOUT`)
만으로 연속 활동 세션이 무한정 유지되던 문제를 로그인 시각 기준 절대 수명
(`SESSION_ABSOLUTE_TIMEOUT`, 기본 12시간)으로 보완했다. 타임아웃 검사를
`admin_security.php:admin_enforce_session_timeout()` 공통 헬퍼로 일원화하고
`dashboard.php`·`monthly_report.php`·`security_evidence.php`·`setup_wizard.php` 전부에서
호출한다(이전엔 대시보드에서만 검사돼 다른 인증 페이지로 우회 가능했던 갭을 닫음).
- **반사형 XSS 차단.** `test_mobile.php` 가 User-Agent 를 그대로 반사하던 것을
`text/plain` + `htmlspecialchars` 로 막았다.
- **입력 검증.** `sensor_data.php` 가 `sensor_id` 범위(TINYINT 1..255)를 경계에서 검사해
500 대신 깔끔한 400 을 반환한다.
- **운영 자가진단 보강.** `ops_checks.php` 가 잔존 `MFA_SETUP_TOKEN` 과 활성화된 레거시
서명 폴백(`ALLOW_LEGACY_BODY_SIGNATURE`)을 경고한다. `config.local.example.php` 에
`ALLOW_LEGACY_BODY_SIGNATURE => false` 를 명시했다.
### DB
- `sql/schema_sht30.sql` · `sql/migration_drop_leak.sql`: 재전송 멱등용 `ingest_nonce`
테이블을 가산·멱등(`CREATE TABLE IF NOT EXISTS`)으로 추가했다.
### 펌웨어 / 빌드 (폐쇄망)
- **호스트 C 패리티 추가(미검증 갭 해소).** `firmware/test/host/sig_cli.c` 와
`parity_test.py` 가 펌웨어 실제 서명 구현(`common/sig.c`)을 호스트 컴파일·실행하여
Python 레퍼런스(`reference.py`)·서버(`verify_signature_raw`)와 **바이트 단위 일치**를
검증한다(C↔Python↔PHP 3자). 이전엔 C 가 어떤 테스트에서도 컴파일되지 않았다.
- **빌드 시 자리표시자 CA 차단.** `build.ps1` 이 `server_ca.c` 의 PLACEHOLDER 를
검출하면 빌드를 중단한다(부팅 시 `bsp_fatal` 로 정지하던 것을 빌드 단계에서 선차단;
`-AllowPlaceholderCA` 로 강행 가능).
- **벤더 핀 엄격 모드.** `vendor.sh`(`VENDOR_STRICT=1`)·`vendor.ps1`(`-Strict`)가 SHA-256
핀 미설정(TODO) 의존성이 있으면 실패한다(릴리스 재현성 강제).
- **stale 주석 정정.** `net.c`/`lwipopts.h` 의 RNG `TODO(hw)` 주석을 정정했다 —
HW RNG `bsp_rand32()` 는 `bsp.c` 에 이미 구현되어 weak 폴백을 덮어쓴다(예측성 완화는
이미 적용됨). `sig.h`/`secrets.h.example` 의 `sha256(key||body)` 표기를 HMAC 로 정정.
### 문서
- **서명 방식 문서 드리프트 일괄 정정(16개 파일).** 운영/보안 문서의
`sha256(API_KEY+body)`(키-접두) 표기를 실제 구현인
`HMAC-SHA256(키=API_KEY, 메시지=요청본문)` 으로 정정했다.
- `SECURITY_DOCUMENT_CONSISTENCY_AUDIT.md` 의 **거짓 일치 인증(false all-clear)** 을
바로잡았다(서명 행을 HMAC 로 갱신 + §2.1 추가).
- `out_of_range` 가 펌웨어 plausibility 전용이며 DB(`sensor_metric.metric_status`)에는
`normal/high_temp/low_temp/high_humidity/low_humidity` 만 저장됨을 명시
(`wiring_diagram.md`, `HARDWARE.md`, `PORTING_NOTES.md`, 펌웨어 `README.md`).
- `BUILD_OFFLINE.md` 의 비실존 스크립트/툴체인 이름 정정(`vendor_deps.* → vendor.*`,
`arm-none-eabi.cmake → arm-none-eabi-toolchain.cmake`). 웹루트 경로가 펌웨어
`app_config.h:APP_API_PATH` 와 일치해야 함을 교차참조.
- HWP 보안대책서 **과대 스코프** 경고를 명문화(온습도 전용; 화재/누수/정전/카메라
미구현). 원본 `.hwp` 는 운영자가 동일하게 정정 필요(`hwp_extracted_text.txt` 배너 +
`SECURITY_PLAN_HWP_REVIEW`/`PASS_READINESS`).
### 테스트
- `run_tests.sh`(전체 러너; php/python3/gcc 부재 시 SKIP 명시) 추가.
- `tests/threshold_sms_test.php`: `sms_truncate_euckr()` 경계 테스트(EUC-KR 멀티바이트).
- `tests/security_hardening_test.php`: 위 개선(nonce/락/SMS캡/태그상수/세션캡/XSS/ops)
소스 회귀 고정.
### 운영자 필수 조치 (코드로 끝낼 수 없는 항목)
- **(CRITICAL)** `firmware/certs/server_ca.c` 를 실제 Cafe24 루트 CA 로 교체(자리표시자
상태로는 부팅 불가; 이제 빌드 단계에서도 차단됨).
- **(HIGH)** 원본 `.hwp` 보안대책서를 온습도 전용 스코프로 정정 후 제출.
- `vendor.{sh,ps1}` 의 SHA-256 핀을 네트워크 빌드 머신에서 채우기(이후 `VENDOR_STRICT`
로 강제). 신규/전환 DB 에 `ingest_nonce` 생성(스키마/마이그레이션 재실행). 운영
`config.local.php` 에서 `MFA_SETUP_TOKEN` 비우기, `ALLOW_LEGACY_BODY_SIGNATURE=false`
확인.
## Unreleased - 보안 하드닝 (전체 코드리뷰 반영)
### ⚠️ 호환성 주의 (BREAKING)
- **요청 서명을 키 접두 SHA256 → HMAC-SHA256 으로 전환했습니다.** 길이확장(length-
extension) 공격에 견디기 위함입니다. `X-Signature = HMAC-SHA256(key=API_KEY, msg=raw_body)`.
서버(`verify_signature_raw`)·펌웨어(`sig.c`)·호스트 레퍼런스(`reference.py`)를 동시에
바꿨으므로, **펌웨어 재플래시와 서버 배포를 함께** 진행해야 합니다(구 펌웨어 ↔ 신 서버는
서명 불일치로 거부됨). 호스트 패리티 테스트와 ARM `-Wconversion -Werror` 컴파일로 검증했습니다.
### 서버 (PHP)
- `api/sensor_data.php`: 재전송(replay) 방어를 추가했습니다 — 본문 `timestamp` 가
`INGEST_MAX_CLOCK_SKEW_SEC`(기본 600초) 범위를 벗어나거나 0 이하이면 거부합니다.
- `api/sensor_data.php`: 운영 비밀값 강제(fail-closed). `API_KEY` 가 예시 기본값이면
수신을 503 으로 거부합니다(`api_key_is_default()`).
- `api/sensor_data.php`: 디바이스 문자열(device_id/sensor_name/device_location/metric_type)
의 제어문자를 제거해 SMS/로그 라인 주입을 막습니다.
- `cron_heartbeat.php`: 오프라인 SMS 중복 발송 경합을 제거했습니다 — `offline_alerted`
0→1 전이를 원자적 조건부 UPDATE 로 선점한 프로세스만 발송합니다.
- `monthly_report.php`: CSV 내보내기의 수식 주입(formula injection)을 방지합니다(자유
텍스트 셀이 `= + - @` 등으로 시작하면 무력화).
- `retention_cleanup.php`: 감사로그 회전을 배타 락(flock)으로 묶어 동시 기록 유실을 막습니다.
- `login.php`: 브루트포스 카운터를 원자적 읽기-수정-쓰기로 변경해 동시 실패 시 카운트
덮어쓰기(lost update)를 막습니다.
- `config.local.example.php`: 예시 TOTP 비밀키를 공개 테스트 값에서 명백히 무효한
플레이스홀더로 교체했습니다(MFA 가 실제 비밀키 설정 전까지 fail-closed).
### DB
- `sql/migration_drop_leak.sql`: information_schema 가드 프로시저로 멱등화했습니다(재실행/
부분 적용 DB 에서도 오류 없이 진행).
### 보안 리뷰 후속 수정 (다중 에이전트 검증에서 발견)
- **(HIGH) 예시 TOTP 비밀키 회귀 수정.** `config.local.example.php` 의
`ADMIN_TOTP_SECRET` 플레이스홀더(`REPLACE_WITH_UNIQUE_BASE32_SECRET`)가 Base32 로
디코드되어(`REPLACEWITHUNIQUEBASE32SECRET` → 18바이트 키) **실제 코드 `275426` 를
생성** = 공개된 시드로 MFA 가 활성화되는 문제였습니다. 빈 문자열 `''` 로 교체해
fail-closed(키 미설정 시 로그인 차단)로 만들고, 회귀 테스트로 고정했습니다.
- **(MEDIUM) 레거시 서명 다운그레이드 경로 차단.** `api/sensor_data.php` 가
`X-Signature` 헤더가 없으면 약한 키-접두 sha256(`verify_signature`)로 폴백하던 것을,
`ALLOW_LEGACY_BODY_SIGNATURE`(기본 false)로 게이트했습니다. 기본값에서는 HMAC 헤더가
없으면 거부합니다(운영 STM32 클라이언트는 HMAC 만 사용).
- **(LOW) 인증 전 본문 크기 상한.** `INGEST_MAX_BODY_BYTES`(기본 8KB) 초과 본문은
json_decode/HMAC 이전에 413 으로 거부 — 미인증 증폭형 DoS 차단.
- **(LOW) heartbeat throttle 원자화.** `check_offline_sensors_throttled()` 의 비원자
read-then-write 를 flock 기반 원자 판정으로 교체 — 동시 호출 시 중복 오프라인 스캔 방지.
- **(LOW) `CRON_SECRET` 강화.** `md5('cron_'.API_KEY)` → `hash_hmac('sha256','cron',API_KEY)`.
- 알려진 잔여(문서화): 600초 창 내 재전송 dedup 부재, 운영 키 보유자에 의한 ingest
flooding(호스트 레벨 rate-limit/WAF 권장), `.htaccess` Apache 전용.
## Unreleased - SHT30 온습도 전용 전환
### 시스템 범위
- 시스템을 서버실 **온습도(SHT30) 24시간 모니터링 + 임계 초과 SMS + 웹 대시보드** 전용으로 전환했습니다. 누수 감지/카메라/사진/사고대응(incident) 기능은 전면 제거했습니다.
- 단일 보드 구성으로 단순화: `sht30_fw`(`sensor_id=2`, `device_id=stm32-sht30-01`)만 사용합니다. 누수 보드(`board_leak`/`leak_fw`)는 제거했습니다.
### 서버 (PHP/DB)
- 온습도 임계 SMS를 추가했습니다: 고온 30℃ / 저온 10℃ / 고습 70% / 저습 20%, 복귀 히스테리시스 ±1℃·±3%, 동일 종류 쿨다운 1800초(30분). 임계는 서버 판정값(`php/config.php` `METRIC_*`, `config.local.php` override)이라 폐쇄망에서 펌웨어 재플래시 없이 변경 가능합니다.
- 측정값을 `sensor_metric`에 저장하고 `metric_status`로 경보/정상복귀를 판정합니다. 장비 오프라인/복구는 `HEARTBEAT_TIMEOUT_SEC`(기본 1200초=20분)으로 판정합니다.
- 사진 업로드/열람 경로(`api/photo_upload.php`, `photo.php`)와 사고 대응(`incidents.php`)을 제거했습니다.
- 신규 설치용 통합 스키마를 `sql/schema_sht30.sql`로 정리했습니다(테이블: `sensor_log`, `sensor_status`, `sensor_metric`, `sms_log`). 기존 누수 설치 전환용 `sql/migration_drop_leak.sql`을 추가해 레거시 누수 컬럼/테이블(`leak_photo`, `leak_incident`, `is_leak` 등)을 정리합니다.
### 펌웨어 (STM32F407 `sht30_fw`)
- 누수 이벤트 본문에서 `is_leak` 와이어 필드를 제거하고 온습도 측정 보고로 단일화했습니다.
- SHT30 I2C 배선을 확정했습니다: I2C1 PB6(SCL)/PB7(SDA), 주소 0x44, 3.3V/GND, 4.7kΩ 풀업. 측정 주기 `APP_SHT30_REPORT_INTERVAL_SEC=300`(5분).
- 레거시 4.7MΩ 누수 풀다운 회로 및 e-con 5C-08 결선 안내를 폐기했습니다.
### 문서
- README, 설치/운영 문서, 배선도, 정리 보고서를 SHT30 온습도 전용으로 재작성했습니다. 누수/사진/카메라/사고대응 관련 절·표를 온습도 임계 SMS/쿨다운/오프라인 기준으로 치환했습니다.
## Unreleased - 메인 MCU 마이그레이션 (Raspberry Pi → STM32F407VGT6)
### 서버 (PHP)
- `sensor_data.php` 인증을 이중 모드로 확장했습니다: `X-Signature` 헤더 기반 raw-body 서명(신규, STM32 펌웨어용)과 기존 본문 필드 서명(RPi, 하위 호환)을 모두 수용합니다.
- `config.php`에 `verify_signature_raw()`를 추가했습니다 — `sha256(API_KEY . 요청본문)`로 검증하여 JSON 재직렬화/부동소수 포맷 차이로 인한 서명 불일치(R1)를 제거합니다.
- raw-body/레거시 서명 회귀 테스트 `tests/raw_body_signature_test.php`를 추가했습니다.
### 펌웨어 (신규 `firmware/`, STM32F407VGT6 / Cortex-M4F)
- 메인 MCU를 Raspberry Pi Zero 2 W(Linux/Python)에서 STM32F407VGT6 베어메탈로 이식했습니다. 구성: 유선 Ethernet(LAN8720 RMII) + LwIP + mbedTLS(HTTPS) + FreeRTOS. WiFi/카메라는 제거.
- 두 보드 변형: `BOARD_LEAK`(누수, sensor_id=1), `BOARD_SHT30`(온습도, sensor_id=2). RPi 2대 구성을 미러링.
- 이식 가능한 코어(`sha256_sw`/`sig`/`jsonbody`/`sht30_convert`/`httpapi`)는 호스트 패리티(Python↔PHP `verify_signature_raw`) + ARM 크로스컴파일(`-Werror -Wconversion`)로 검증했습니다.
- 다중 에이전트 코드리뷰에서 확인된 결함을 수정: TLS 인증서 유효기간 검증 활성화(RTC/SNTP 시간 공급), FreeRTOS 정적메모리 콜백 중복정의 제거, LwIP 코어락 위반(PHY 링크/DNS) 수정, 로그 UART PA2 충돌 → USART3(PD8/PD9) 이동, EXTI 핸들러 추가, HW RNG 기반 `bsp_rand32`.
- 폐쇄망 빌드: 의존성은 `firmware/third_party`에 벤더링, 오프라인 빌드/플래시 절차 문서화(`firmware/docs/BUILD_OFFLINE.md`).
- `docs/stm32f407_migration_plan.md`(계획+구현 현황), `firmware/docs/{HARDWARE,PORTING_NOTES}.md`를 추가했습니다.
- 레거시 RPi 펌웨어(`raspberry_pi/`)와 systemd 유닛(`systemd/`)을 제거했습니다(STM32 펌웨어로 대체).
## v2605 - 2026-05-19
- v2605 정리 전 원본을 `_backup/pre_v2605_20260519/`에 보관했습니다.
- PHP/RPi 운영 비밀값을 코드 하드코딩에서 로컬 설정/환경 변수 방식으로 분리했습니다.
- `config.local.example.php`, `config.example.py`, `leak-sensor.env.example`을 추가했습니다.
- 누수경보 SMS 쿨다운이 실제 발송 로직에 적용되도록 수정했습니다.
- 누수 지속 SMS를 시간 기반 5분 정책으로 정리했습니다.
- heartbeat 체크 주기와 오프라인 판정 시간을 분리했습니다.
- 사진 업로드가 실제 `leak_alert` 로그 ID를 검증하도록 강화했습니다.
- 신규 설치용 통합 DB 스키마 `sql/schema_v2605.sql`을 추가했습니다.
- README와 보고서를 실제 코드 기준으로 최신화했습니다.
- 누수 사고 대응 테이블과 대시보드 사고 패널을 추가했습니다.
- 누수 사고 `확인함`, `출동 중`, `조치 완료`, `오탐` 상태 변경을 지원합니다.
- 미확인 누수 사고 SMS 에스컬레이션을 추가했습니다.
- 대시보드 첫 화면을 위험 상태 우선 구조로 재배치했습니다.
- 운영 자가진단 패널을 추가해 설정값, 마이그레이션, 권한, SMS 실패를 표시합니다.
- 30일 운영 요약을 추가해 사고 상태, 평균 확인/조치 시간, SMS 실패, 사진 저장 현황을 보여줍니다.
- 설치/운영 점검 화면 `php/setup_wizard.php`를 추가했습니다.
- 보안대책서 첨부용 증적 보고서 `php/security_evidence.php`와 운영·보안 체크리스트를 추가했습니다.
- 월간 보고서 화면 `php/monthly_report.php`를 추가했습니다.
- 누수 사진 타임라인 다중 촬영을 추가했습니다.
- 사진 타임라인 DB 컬럼과 기존 설치용 `sql/migration_photo_timeline.sql`을 추가했습니다.
- Pi 증적 수집 스크립트를 추가해 API 키 원문 없이 서비스/포트/환경파일 상태를 확인할 수 있게 했습니다.