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

23 KiB

변경 이력

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 키 원문 없이 서비스/포트/환경파일 상태를 확인할 수 있게 했습니다.