POSA_LEAKSMS/docs/DEPLOYMENT_PLAN_RPI4x3.md
cw2007 fb3630d868 docs: 문서 현행화(leak_ 접두사·경로 스크럽) + 코드 투어 + settings deny 수정
- 전 운영 문서 테이블명 leak_ 접두사 반영, 삭제된 마이그레이션 참조 정리,
  구경로 raspi_leck_detecter→<WEB_DIR> 스크럽(DEPLOYMENT_PLAN).
- docs/DEPLOYMENT_PLAN_RPI4x3.md(신규): 3대 Pi + 신규 서버 배포 로드맵.
- .tours/architect-full-system.tour(신규): 19스텝 전체 아키텍처 투어.
- README.md(루트, 0바이트였음): 시스템 인덱스.
- .claude/settings.local.json: /doctor 지적 deny 규칙 5건 수정
  (파이프 4건 와일드카드형, 포크밤 삭제) + 죽은 allow 항목 3건 정리.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821XP36Q6t9eWKTPSEumnC
2026-07-02 18:52:15 +09:00

28 KiB
Raw Blame History

배포 계획: 누수+온습도 통합 모니터링 시스템 (cafe24 서버 1 + Raspberry Pi 4 × 3)

기준 저장소: C:/Users/USER/Desktop/complete/leak_sms_v3 확정 조건: RPi4 3대 모두 신품(OS 없음, SD 새로 플래시), 설치 현장 인터넷 가용(오프라인 vendor 번들은 선택이나 재현성 위해 권장), cafe24 서버 신규 설치.


1. 전체 로드맵

순서 원칙: 서버 먼저. 모든 Pi의 test_connection.py 검증과 API_KEY 발급이 서버에 의존하므로 서버가 선행되어야 한다 (raspberry_pi4/config_local.example.py, raspberry_pi4_sht30/README.md — API_KEY는 서버 config.local.php와 byte-단위 동일해야 함).

[0단계] cafe24 서버 신규 설치 ──────────────► API_KEY 확정, 엔드포인트 URL 확정
        │
        ├─ (병렬 가능) SD 카드 3장 OS 플래시 + 공통 OS 프로비저닝
        │   ※ 서버와 의존성 없음 — 서버 작업과 동시 진행 가능
        │
        ▼
[1단계] Pi-A (모듈1: 누수+카메라, sensor_id=1)  ┐
[2단계] Pi-B (모듈2: SHT30, sensor_id=2)        ├─ 서로 독립, 병렬 설치 가능
[3단계] Pi-C (sensor_id=3, 신규 지점)           ┘  (단, Pi-C는 센서 종류 결정 선행 — §6)
        │
        ▼
[4단계] end-to-end 검증 (대시보드 / SMS / 사진 / 오프라인 감지) + 보안 체크리스트 (§7)

병렬 가능 구간:

  • 서버 설치(0단계)와 Pi 3대의 OS 플래시·공통 프로비저닝(§3)은 완전 병렬.
  • Pi-A / Pi-B / Pi-C의 모듈 설치는 서로 독립 — 서버의 API_KEY만 확정되면 동시 진행 가능.
  • (선택) 오프라인 vendor 번들 생성(vendor.sh)은 인터넷 연결된 aarch64 Bookworm Pi 1대에서 선행 — 첫 번째로 부팅한 Pi에서 만들어 나머지에 복사하는 흐름 권장 (raspberry_pi4/vendor.sh, raspberry_pi4_sht30/vendor.sh의 .deb은 타깃 아키텍처에서만 받을 수 있음).

Pi-C 블로커: 센서 종류(누수 / SHT30 / 둘 다) 미결정 → §6의 작업량 비교로 먼저 결정 필요.


2. 0단계: cafe24 서버 신규 설치

2.1 DB 스키마

  • cafe24 phpMyAdmin(또는 MySQL 콘솔)에서 sql/schema_sht30.sql 1회 실행. 신규 설치는 이 파일 하나로 완결 — leak_sensor_log, leak_sensor_status, leak_sensor_metric, leak_sms_log, leak_ingest_nonce, leak_incident, leak_photo 및 leak 컬럼/ENUM 전부 포함 (sql/schema_sht30.sql:25-167).
  • 마이그레이션 파일 없음 — v2607 테이블 리네임(leak_ 접두사)과 함께 폐기. 신규 설치는 스키마 파일 하나로 완결.

2.2 파일 업로드

  • php/ 전체를 웹루트 public_html/<WEB_DIR>/에 업로드 (docs/INSTALL_PI_SERVER.md §3, php/.htaccess 헤더).
  • 주의: docs/INSTALL_PI_SERVER.md:33-56의 업로드 목록은 STALE — api/photo_upload.php, photo.php, var/.htaccess, var/index.html, var/photos/가 누락되어 있음. 누수 사진 파이프라인에 필수이므로 php/ 트리 전체를 기준으로 업로드할 것.
  • php/var/ 를 PHP 쓰기 가능하게 설정 (heartbeat_last.txt, ingest_rejections.json, login_attempts.json, admin_audit.log 저장). 코드 기준 디렉터리 0750 / 파일 0640 (php/api/photo_upload.php:303,321) — cafe24 공유호스팅에서 755/775가 필요할 수 있음(갭, §8). var/photos/는 첫 업로드 시 자동 생성 + .htaccess 가드 자동 작성 (php/api/photo_upload.php:139-153, 301-321).
  • 웹에서 /var/ 접근이 403인지 확인 (php/.htaccess:14 + php/var/.htaccess).

2.3 config.local.php / API_KEY

  • php/config.local.example.php 복사 → config.local.php 작성:
    • DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASS — cafe24 MySQL 자격증명
    • API_KEY — 긴 랜덤 비밀키 생성. 기본값 'change-this-api-key'이면 ingest가 503으로 거부됨(fail-closed, php/api/sensor_data.php:73-77). 이 키가 Pi 3대 전부의 config_local.py API_KEY와 byte-단위 동일해야 함.
    • ALLOW_LEGACY_BODY_SIGNATURE — false 유지 (php/config.php:60, php/ops_checks.php:101)
    • SMS_USER_ID / SMS_SECURE / SMS_SENDER / SMS_RECIPIENTS — cafe24 SMS 계정, 사전 등록된 발신번호, 잔액 충전 필요 (게이트웨이 php/config.php:66)
    • 임계값: METRIC_TEMP_HIGH_C=27, METRIC_RH_HIGH=60, METRIC_RH_LOW=-1(저습 비활성), METRIC_DEWPOINT_MARGIN_C=0(결로 비활성) — 예시값 기준 (php/config.local.example.php:44-50, commit c8b448c)

2.4 관리자 인증 (fail-closed — 미설정 시 로그인 자체가 불가)

  • php setup_hash.php "새-관리자-비밀번호" 실행 → bcrypt 출력물을 ADMIN_PASSWORD_HASH에 (docs/INSTALL_PI_SERVER.md:135-141)
  • MFA 등록: MFA_SETUP_TOKEN에 임시 랜덤값 설정 → setup_mfa.php?token=<토큰> 접속 → TOTP 앱 등록 → 표시된 ADMIN_TOTP_SECRET을 config.local.php에 → MFA_SETUP_TOKEN 반드시 비우기 (docs/INSTALL_PI_SERVER.md:100-106, php/ops_checks.php:100)
  • ADMIN_TOTP_SECRET은 서버에서 생성하기 전까지 절대 자리표시자 값을 넣지 말 것 (php/config.local.example.php:33-40)

2.5 HTTPS / cron

  • HTTPS 리다이렉트는 cafe24 호스팅 관리 패널에서 설정 — .htaccess 리다이렉트는 리버스프록시 뒤에서 무한루프 위험으로 의도적으로 비활성 (php/.htaccess:6-8, docs/SOURCE_SECURITY_EVIDENCE.md §2.1). 세션 쿠키가 Secure라 HTTPS 없이는 로그인 불가.
  • cron 1분 주기: CLI형 * * * * * php /home/<user>/public_html/<WEB_DIR>/cron_heartbeat.php. HTTP형 사용 시 ?key=<CRON_SECRET> 필수 — CRON_SECRET = hash_hmac('sha256','cron',API_KEY) (php/config.php:199, php/cron_heartbeat.php:163-169). 이 값은 UI 어디에도 표시되지 않아 직접 계산해야 함(갭). docs/INSTALL_PI_SERVER.md:165의 URL 예시는 key 파라미터가 빠져 있어 그대로 쓰면 403.
  • (선택) 보존기간 정리: php retention_cleanup.php --execute CLI 전용 (php/retention_cleanup.php:11-16) — cron 등록 안 하면 365일 보존이 실제로 집행되지 않음.

2.6 서버 검증

  • login.php 로그인(비밀번호+TOTP) → setup_wizard.php에서 전체 점검: DB 연결, 필수 테이블, API_KEY 교체 여부, 관리자 해시/TOTP, MFA 토큰 제거, 레거시 서명 비활성, SMS 수신자, 임계값, var 쓰기권한, 테스트 SMS 발송 버튼 (php/setup_wizard.php:29-45, php/ops_checks.php:44-111)
  • 주의: setup_wizard는 4개 코어 테이블만 검사 — leak_incident/leak_photo/leak_ingest_nonce 누락은 감지 못 하고 조용히 기능 저하됨 (php/ops_checks.php:51). 스키마 실행 후 phpMyAdmin에서 7개 테이블 존재를 직접 확인할 것.
  • CORS 오리진이 https://pores.kr로 하드코딩되어 있음 (php/api/sensor_data.php:49, php/api/photo_upload.php:29) — 다른 도메인 배포 시 수정 필요(갭).

3. 공통: 새 Pi OS 프로비저닝 (3대 공통)

3.1 문서화된 요구사항 (repo에 근거 있음)

항목 값 근거
OS Raspberry Pi OS Bookworm 64-bit (aarch64) raspberry_pi4/install.sh:4, raspberry_pi4_sht30/install.sh:4, 양쪽 README §2
Python 시스템 3.11 (Bookworm 기본; wheel이 cp311로 핀) vendor.sh TARGET_PYVER=311
사용자 pi (sudo 가능) — systemd 유닛 User=pi, 경로 /home/pi/* 하드코딩 양쪽 systemd/*.service, install.sh SERVICE_USER 기본값
네트워크 유선 Gigabit 또는 WiFi, 서버로 HTTPS egress 양쪽 README §2
시각 동기 NTP 필수 — 서버가 타임스탬프 ±600초 밖이면 거부 raspberry_pi4_sht30/README.md:61, php/config.php:45
I2C (Pi-B) install.sh가 자동 처리 (dtparam=i2c_arm=on 등) — raspi-config는 수동 폴백만 raspberry_pi4_sht30/install.sh:62-88

3.2 문서에 없어 현장에서 결정해야 하는 것 (전부 갭 — repo에 절차 없음)

  • SD 이미징 절차 없음: OS 이미지 버전/체크섬/Imager 사용법 미문서화. → 결정: Raspberry Pi Imager 등으로 Bookworm 64-bit(Lite 권장 여부도 미명시)를 플래시하되, 사용한 이미지 버전을 기록해 3대 동일하게 맞출 것.
  • pi 사용자 생성 절차 없음: Bookworm 이미지는 기본 pi 계정이 없음 — Imager 커스텀 설정 또는 userconf.txt로 pi 계정을 만들어야 systemd 유닛/경로가 무수정으로 동작. 비밀번호 정책 미문서화.
  • SSH/헤드리스 활성화 절차 없음: Imager 설정 또는 boot 파티션 ssh 파일 방식 중 결정 필요.
  • 호스트네임 규약 없음: DEVICE_ID(rpi4-leak-01 등)는 애플리케이션 식별자일 뿐 OS 호스트네임 아님. → 결정 권장: 호스트네임을 DEVICE_ID와 일치시켜 운영 혼선 방지.
  • 네트워크 설정(고정 IP vs DHCP, WiFi 프로비저닝) 없음.
  • 로케일/타임존, apt 보안 업데이트 정책, OS 하드닝 절차 없음 (§7·§8 참조).

3.3 (선택·권장) 오프라인 vendor 번들

현장에 인터넷이 있으므로 install.sh 온라인 경로로도 설치 가능하나, 재현성을 위해:

  • 첫 부팅한 Pi(인터넷 연결, aarch64 Bookworm) 1대에서 각 모듈의 ./vendor.sh 실행 → wheelhouse/ + debs/ 생성. .deb은 반드시 타깃 아키텍처(aarch64 Bookworm)에서만 받을 수 있음 (vendor.sh:7-10,55) — x86 빌드머신 불가.
  • 생성된 번들을 나머지 Pi의 모듈 폴더에 복사 → install.sh가 자동으로 오프라인 모드 진입(불완전 번들은 하드 에러, 온라인 폴백 없음).
  • 갭: 번들 무결성 해시 검증 절차 없음 — sha256sum 목록을 직접 만들어 전달 권장.

4. Pi-A: 모듈 1 (누수 + Pi Camera, sensor_id=1)

소스: raspberry_pi4/ — 근거는 raspberry_pi4/README.md, install.sh, config.py, systemd/leak-sensor.service.

4.1 배선 (README.md §2.1)

  • e-con 5C-08 누수센서 (~1.5MΩ 고임피던스): 센서 입력 → GPIO17(BCM) = 물리핀 11, 3.3V → 핀 1, GND → 핀 6
  • 외부 4.7MΩ 풀다운 필수, 내부 풀 OFF 유지 (config.py USE_INTERNAL_PULL=False → PUD_OFF; 내부 50kΩ 풀다운이 1.5MΩ 센서 신호를 죽임 — README.md:49-53)
  • 로직: ACTIVE_LOW=False — 건조=LOW, 누수=HIGH; 디바운스 500ms (config.py:20-24)
  • Pi Camera: Pi 4의 15핀 1.0mm CSI 커넥터 (Zero 2 W용 22핀 리본 비호환 — 15핀 리본 또는 어댑터 필요). 전원 USB-C 5V/3A(카메라 사용 시 여유 권장).

4.2 설치

  • 폴더를 /home/pi/raspberry_pi4에 배치 (유닛 파일이 경로 하드코딩 — systemd/leak-sensor.service:12-13,26-27; 다르면 유닛 수정)
  • cp config_local.example.py config_local.py 후 5개 값 기입:
    • API_URL = https://<서버>/<WEB_DIR>/api/sensor_data.php
    • PHOTO_UPLOAD_URL = .../api/photo_upload.php
    • API_KEY = 서버 config.local.php와 byte-단위 동일
    • DEVICE_ID = 예: rpi4-leak-01, DEVICE_LOCATION = 설치 장소
    • 누락 시 fail-closed로 기동 거부 (config.py:69-83); 절대 커밋 금지 (.gitignore)
  • chmod +x install.sh && ./install.sh — 온라인: apt로 python3-rpi-lgpio python3-requests python3-venv rpicam-apps(또는 libcamera-apps) 설치; 오프라인 번들 있으면 자동 오프라인. venv(--system-site-packages) + requests==2.32.3, 로그파일 /var/log/leak_sensor.log (640, pi 소유) 생성.
  • 주의(모순 문서): raspberry_pi4/README.md §3(57-68행)의 "php/ 서버는 누수 이벤트 거부, 레거시 서버로 향하라"는 안내는 STALE — config_local.example.py:8-16과 머지된 PR #1 기준 통합 서버가 정답. README §3을 따르지 말 것.

4.3 systemd

  • sudo cp systemd/leak-sensor.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable leak-sensor && sudo systemctl start leak-sensor
  • 유닛 특성: User=pi, SupplementaryGroups=video(카메라 접근 필수), Restart=always/10s, PrivateTmp=true, network-online.target 이후 기동

4.4 End-to-end 검증

  • source venv/bin/activate && python3 test_connection.py — 3단계: test.php POST / 무서명 POST(거부 확인) / HMAC 서명 POST(200 확인)
  • 수동 실행 python3 leak_sensor.py → startup 이벤트가 서버 sensor_log에 기록되는지 대시보드 확인
  • 카메라 사전 검증(갭 — 문서화된 절차 없음): 설치 절차에 카메라 감지 확인이 없어 첫 누수 때에야 실패가 드러남. rpicam-still로 수동 테스트샷을 찍어볼 것을 권장.
  • 누수 모의 시험: 센서 단락 → 대시보드 누수 카드(sensor_id=1 전용, php/dashboard.php:256-264) 표시 + [누수감지] SMS + leak_incident 행 + photo.php?id=N으로 사진 열람(관리자 세션 필요)
  • systemctl status leak-sensor, journalctl -u leak-sensor -f 정상 확인

5. Pi-B: 모듈 2 (SHT30 온습도, sensor_id=2)

소스: raspberry_pi4_sht30/ — 근거는 해당 README.md, install.sh, config.py, systemd/sht30-sensor.service.

5.1 배선 (README.md §2.1)

  • Sensirion SHT30 (한진데이타 P4422-3), I2C 버스 1: VCC → 3.3V 핀 1 (5V 금지), GND → 핀 6 등, SDA → GPIO2/핀 3, SCL → GPIO3/핀 5
  • ADDR → GND = 주소 0x44 (VDD면 0x45 — config.py I2C_ADDR와 일치시킬 것)
  • SDA/SCL에 3.3V 풀업 4.7kΩ ×2 (모듈 온보드 풀업 있으면 생략)

5.2 설치

  • 폴더를 /home/pi/raspberry_pi4_sht30에 배치 (유닛 경로 하드코딩)
  • cp config_local.example.py config_local.py → API_URL(Pi-A와 동일 엔드포인트) / API_KEY(동일 키) / DEVICE_ID(예: rpi4-sht30-01) / DEVICE_LOCATION 기입 (fail-closed)
  • ./install.sh — [1/4] apt: python3-requests python3-venv i2c-tools; [2/4] I2C 자동 활성화: /boot/firmware/config.txt에 dtparam=i2c_arm=on, i2c-dev 모듈 로드+영속화, usermod -aG i2c pi; [3/4] venv + requests==2.32.3, smbus2==0.5.0; [4/4] /var/log/sht30_sensor.log
  • install.sh가 리부트 안내를 출력하면 sudo reboot (I2C 신규 활성화 시 — README §4.2). 주의(갭): usermod -aG i2c도 재로그인/리부트 후 반영 — 리부트 없이 곧바로 test_sensor.py를 돌리면 /dev/i2c-1 권한 오류 가능.

5.3 systemd

  • sudo cp systemd/sht30-sensor.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable sht30-sensor && sudo systemctl start sht30-sensor
  • 유닛 특성: User=pi, SupplementaryGroups=i2c, Restart=always/10s, PrivateTmp=true. install.sh는 유닛을 설치하지 않으므로 이 수동 단계 누락 금지(갭).

5.4 End-to-end 검증

  • i2cdetect -y 1 → 0x44 표시 확인 (README §5)
  • python3 test_sensor.py (CRC/포맷 자가검사 + 1회 측정) — 주의: 하드웨어 없이도 시뮬레이션으로 통과하므로, i2cdetect 확인을 반드시 선행. 시뮬레이션 폴백이 배선 오류를 가릴 수 있음(갭): 서비스 로그에 시뮬레이션 WARN이 없는지 journalctl로 확인할 것.
  • python3 test_connection.py — 무서명 400/403, 서명 200; 403이면 API_KEY 불일치, 400이면 NTP/타임스탬프 의심
  • 수동 실행 후 startup 이벤트 → 이후 300초 주기 보고. 대시보드에서 sensor_id=2 카드 + 차트(기본 sensor 2, php/dashboard.php:125-129) + leak_sensor_metric 5분 축적 확인
  • 임계값 시험: 고온/고습 유발 → 태그된 SMS + 30분 쿨다운 + 회복 SMS 확인

6. Pi-C: sensor_id=3 신규 지점 (extension-pattern 기준)

근거: docs/solutions/best-practices/sensor-telemetry-extension-pattern-2026-05-21.md (단, 이 문서 자체가 legacy 선언 — 현행 코드 기준으로 재검증된 아래 내용을 따를 것).

6.1 그냥 되는 것 (서버 무변경)

  • 등록 절차 없음: sensor_id는 HMAC 서명된 본문으로 도착, 1..255 범위검사 후 leak_sensor_status에 UPSERT로 자동 등록 (php/api/sensor_data.php:199-205, 316-340). SQL 마이그레이션 불필요.
  • 온습도 보고 시 metric 저장, 임계값 SMS/회복 SMS(센서별 쿨다운), 오프라인/온라인 SMS, 에스컬레이션, stuck 감지, 월간 리포트 GROUP BY — 전부 sensor_id 단위로 자동 동작 (php/cron_heartbeat.php:22-83, php/sms_send.php:426-509).
  • 대시보드 센서 상태 카드 목록에는 sensor 3이 자동 표시 (php/dashboard.php:87, 635-690).
  • 누수 이벤트도 서버 처리 자체는 됨: [누수감지] SMS + leak_incident 생성 + 사고 테이블 표시 (php/sms_send.php:546-608, php/api/sensor_data.php:371-411, incident 쿼리는 무필터).

6.2 코드/스키마 작업이 필요한 것

항목 내용 근거
차트 sensor_id=2 하드코딩, 선택 UI 없음 — sensor 3 이력은 저장되지만 차트로 못 봄 php/dashboard.php:126-129
최근 온습도 카드 차트 센서(=2)의 최신값만 표시 php/dashboard.php:218-220, 596-601
30일 평균 전 센서 혼합 집계(GROUP BY 없음) — 지점이 다른 sensor 3이 들어오면 평균이 무의미해짐 php/dashboard.php:210-216
누수 상태 카드 sensor_id===1 하드코딩 — Pi-C가 누수형이면 실시간 카드 미표시 php/dashboard.php:256-264, 693
임계값/쿨다운 METRIC_* 전역 단일값 — 지점별 임계 필요 시 코드/스키마 변경 php/config.php:134-153
하트비트 타임아웃 전역 1200초 — 누수형 Pi-C(보고 주기 3600초)는 매 사이클 오프라인 오탐 → 전역 상향 시 5분 센서의 감지 지연 php/config.php:197, raspberry_pi4/config.py REPORT_INTERVAL=3600
SENSOR_ID 위치 (SHT30형) git 추적되는 config.py:16-17을 직접 수정해야 함 — config_local.py에 넣어도 무시됨(import 목록 고정, config.py:70-76) drift 위험

6.3 센서 종류별 작업량 비교

SHT30형 (권장 최소) 누수형 둘 다 (한 Pi)
Pi측 raspberry_pi4_sht30/ 복사, config.py SENSOR_ID=3/SENSOR_NAME 수정(추적 파일!), config_local.py 작성(DEVICE_ID 예: rpi4-sht30-02) raspberry_pi4/ 복사, config.py SENSORS에 {"id":3,"gpio":22,...} 추가(주석 예시 기존재, config.py:17), config_local.py + PHOTO_UPLOAD_URL 두 모듈 폴더 모두 설치 — 핀 충돌 없음(PR_BODY.md §7). 단 두 모듈이 각각 sensor_id를 가지므로 id 3·4 두 개를 쓸지 결정 필요(갭 — 문서화된 패턴 없음)
서버측 필수 없음 없음 (수신·SMS·incident 동작) 없음
서버측 권장 수정 차트/최근값 카드가 sensor 2 고정 → 보고 싶으면 dashboard.php 수정; 30일 평균 혼합 문제 + 누수 카드 sensor 1 고정 수정, HEARTBEAT_TIMEOUT_SEC 충돌 해결(가장 큰 이슈) 양쪽 전부
결론 작업량 최소 — 대시보드 차트만 감수하면 무수정 운영 가능 서버 무수정 시 오프라인 오탐 + 대시보드 비가시 감수 필요 최대 작업량

공통 주의: sensor_status는 sensor_id 단독 키 — Pi-C가 실수로 id 1/2를 재사용하면 기존 지점 상태를 조용히 덮어씀 (php/api/sensor_data.php:316-340). DEVICE_ID는 표시용일 뿐 인증 정체성이 아님.


7. 배포 시점 보안 체크리스트 (repo 근거 항목만)

서버 (근거: docs/OPERATIONS_SECURITY_CHECKLIST.md, docs/SOURCE_SECURITY_EVIDENCE.md, php/ops_checks.php)

  • API_KEY 기본값 교체 확인 (기본값이면 ingest 503 — php/api/sensor_data.php:73-77)
  • ALLOW_LEGACY_BODY_SIGNATURE=false (php/ops_checks.php:101)
  • MFA_SETUP_TOKEN 비움 확인 (php/ops_checks.php:100)
  • HTTPS를 cafe24 패널에서 강제 + 인증서/리다이렉트 화면 증적 캡처 (docs/SOURCE_SECURITY_EVIDENCE.md §2.1 — .htaccess는 의도적으로 리다이렉트 안 함, 조용히 누락되기 쉬운 수동 단계)
  • 위조 서명 403 / 정상 서명 200 테스트 증적 (docs/SOURCE_SECURITY_EVIDENCE.md §5)
  • config.local.php 등 민감 파일 직접 접근 차단 확인 (php/.htaccess FilesMatch — docs/SOURCE_SECURITY_EVIDENCE.md §2.8)
  • php/var/admin_audit.log에 로그인/로그아웃/MFA 이벤트 기록 확인
  • curl -I https://<도메인>/<WEB_DIR>/login.php 응답 헤더 점검 (checklist §3)
  • SMS 잔액/발신번호/수신자 최신성 확인; 백업 + 복원 테스트(php scripts/backup_evidence.php) — checklist §9

Pi 3대 공통

  • config_local.py 커밋 금지(.gitignore 확인); 노출 시 서버·전 노드 동시 API_KEY 교체 (양쪽 README §7)
  • 단일 공유 API_KEY 구조 인지: Pi 1대 탈취 = 전 지점 ingest 자격 노출, 기기별 폐기 불가 (extension-pattern 갭) — 지점 추가마다 노출면 확대
  • 기기 인바운드 차단 증적: ss -lntup으로 불필요 리스닝 데몬 없음 확인 (docs/SOURCE_SECURITY_EVIDENCE.md §4 — 단 달성 방법은 미문서화, §8 갭)
  • systemctl status leak-sensor(및 sht30-sensor) 화면 증적 (§5 최소 증적 세트)
  • NTP 동기 확인 (±600초 초과 시 전 POST 거부)

repo에 없어 자체 수립 필요 (문서화 갭 — §8): Pi 계정/SSH 하드닝, 호스트 방화벽(ufw 등), 시크릿 파일 권한(chmod 600), OS 패치 정책.


8. 문서화 갭 / 결정 필요 사항 (통합·우선순위)

P0 — 배포를 잘못되게 만들 수 있는 것

  1. STALE 문서 모순 — 모듈1 대상 서버: raspberry_pi4/README.md §3·§7은 "php/ 서버가 누수 거부 / 레거시 sha256 서명"이라 함. 현행은 통합 서버 + raw-body HMAC (config_local.example.py:8-16, SYSTEM_OVERVIEW.md, PR #1). README만 보고 배포하면 오설정. → README §3/§7 갱신 필요.
  2. docs/INSTALL_PI_SERVER.md 업로드 목록 STALE: photo_upload.php/photo.php/var 가드 누락 (33-56행) — 그대로 따르면 누수 사진 파이프라인 결손. 또한 이 문서 자체가 STM32(v2606) 중심이며 RPi4 신규 설치 통합 가이드는 부재 (root README.md는 0바이트).
  3. cron URL 오류: INSTALL_PI_SERVER.md:165에 ?key=<CRON_SECRET> 누락 → 403. CRON_SECRET 표시 UI 없음, 수동 계산 필요.
  4. 신품 Pi 프로비저닝 절차 전무: SD 이미징, pi 계정 생성(Bookworm 기본 미존재), SSH 활성화, 호스트네임, 네트워크 설정 — 전부 미문서화. 결정 필요: 이미지 버전, 계정/비밀번호 정책, 고정IP vs DHCP, 호스트네임 규약.
  5. Pi-C 센서 종류 결정: §6.3 비교 기준으로 결정. 누수형 선택 시 HEARTBEAT_TIMEOUT_SEC 충돌(전역 1200초 vs 보고 3600초)을 반드시 먼저 해결.

P1 — 운영 중 사고로 이어질 수 있는 것

  1. Pi OS 하드닝 부재: 계정/SSH/방화벽/시크릿 파일 권한(600)/패치 정책이 4개 보안 문서 어디에도 없음 — docs/SOURCE_SECURITY_EVIDENCE.md도 "코드로 증명 불가, 기관 자료 필요"로만 표기. → 자체 표준 수립 필요.
  2. 단일 공유 API_KEY / 센서 레지스트리 부재: 키 보유자는 임의 sensor_id 생성·기존 id 덮어쓰기 가능; 기기별 폐기 불가. 지점이 늘수록(Pi-C) 위험 증가 — device_id↔sensor_id 서버측 바인딩 검토 권장.
  3. 시뮬레이션 폴백이 오류 은폐: SHT30 모듈은 I2C 실패 시 그럴듯한 더미값을 운영 서버로 전송(WARN 로그만) — FORCE_REAL 옵션 없음. 배포 검증 시 journalctl 확인을 절차화할 것.
  4. NTP 의존 미절차화: 서버가 ±600초 강제하는데 시각동기 확인 단계가 설치 절차에 없음 (현장 인터넷 가용이라 기본 NTP로 충족 예상되나 확인 단계 추가 권장).
  5. 카메라 검증 단계 부재 (Pi-A): 첫 누수 때에야 실패 발견 — 설치 시 테스트샷 절차 추가 필요.

P2 — 정리/개선

  1. 설정 규약 이원화: docs/SHT30_SENSOR2_SETUP.md §3과 php/ops_checks.php:97은 /etc/sht30-sensor.env를 참조하나 실제 모듈은 config_local.py만 읽음 — 문서 갱신 필요.
  2. SENSOR_ID가 추적 파일(config.py)에 위치 (SHT30형): config_local 오버라이드 허용하는 소규모 수정 권장 (Pi-C에서 실익).
  3. systemd 유닛 경로/User=pi 하드코딩 2개 파일 — 비표준 배치 시 수동 수정 2곳.
  4. 센서 폐기 UI 없음: leak_sensor_status 행 수동 SQL 삭제 전까지 영구 오프라인 표시.
  5. 임계값 문서 드리프트: SOURCE_SECURITY_EVIDENCE.md §2.4(30℃/70%)와 현행 권장값(27℃/60%, commit c8b448c) 불일치.
  6. retention_cleanup cron 미문서화; 오프라인 번들 해시 검증 부재(--require-hashes 없음); CORS pores.kr 잔재; 로그 권한 실패 시 조용한 콘솔 폴백(양 모듈); var/ 권한 비트 미문서화(cafe24에서 setup_wizard '상태 파일 폴더' 체크로 확인).
  7. firmware/(STM32) 트랙은 이번 배포와 무관 — RPi4 배포에 불필요 (docs/stm32f407_migration_plan.md는 별도 마이그레이션 트랙, 링크/실행 미검증 상태).

9. [확정] Pi-C = 누수 + 카메라 (sensor_id 3) — 결정 반영 부록

✅ v2607 구현 완료: §9.2 필수 코드 작업 3건 전부 반영됨 — REPORT_INTERVAL=600(하트비트 오탐 해소), 대시보드 LEAK_SENSOR_IDS 루프(누수 카드 다중 지점), config_local.py SENSORS 오버라이드(+형식 검증 fail-closed). 테이블은 전부 leak_ 접두사로 리네임됨. 배포는 tools/make_deploy_bundles.py 번들 사용.

사용자 결정(2026-07-02): Pi-C는 누수 + 카메라 구성.

9.1 누수형 선택의 이점 (SHT30형 대비)

  • 대시보드 차트 sensor 2 하드코딩 문제(§6.2) 해당 없음 — Pi-C는 metric을 안 보냄.
  • 30일 평균 혼합 오염(§6.2) 해당 없음 — 동일 이유.
  • SENSOR_ID 추적파일(config.py) 문제는 누수 모듈도 동일하나, raspberry_pi4/config.py:13 SENSORS 리스트에 id=3 항목 추가 형태라 구조는 명확.

9.2 필수 코드 작업 (검증 완료 — file:line 확인됨)

  1. 하트비트 오탐 (P0, Pi-A에도 이미 존재하는 버그)
    • php/cron_heartbeat.php:25-30 — leak_sensor_status 전체를 전역 HEARTBEAT_TIMEOUT_SEC(기본 1200s, php/config.php:197)로 검사. 필터 없음.
    • raspberry_pi4/config.py:35 — 누수 모듈 정상 주기 보고 3600s.
    • 결과: 누수 노드(id 1, id 3)는 매 사이클 오프라인 SMS 오탐 → 복귀 SMS 반복.
    • 권장 수정(택1):
      • (a) 최소 변경: 누수 모듈 REPORT_INTERVAL 3600 → 600 (Pi-A·Pi-C 공통). 주기 POST는 소형 JSON이라 부하 무시 가능. 서버 무수정.
      • (b) 구조적: sensor_status에 per-sensor timeout 컬럼 추가 + cron_heartbeat/dashboard 반영 (스키마+코드 변경).
  2. 대시보드 누수 카드 (Pi-C 가시성)
    • php/dashboard.php:260 — (int)$s['sensor_id'] === 1 하드코딩. Pi-C 누수 상태 카드 미표시.
    • 수정: is_leak 값이 있는 모든 센서에 대해 카드 렌더 (사고 테이블은 이미 무필터 — 수정 불요).
  3. Pi-C 설정값
    • raspberry_pi4/config.py SENSORS에 {"id": 3, ...} (gpio 17 유지 — 별도 Pi이므로 핀 충돌 없음), DEVICE_ID 예: rpi4-leak-02, DEVICE_LOCATION = 신규 지점명.

9.3 Pi-C 추가 하드웨어 (구매 확인 필요)

  • e-con 5C-08 누수센서 1개 + 4.7MΩ 풀다운 저항
  • Pi Camera (15핀 CSI 리본) 1개
  • SD카드·USB-C 5V/3A 전원 (3대 공통)

9.4 보안 유의 (extension-pattern 갭)

  • 단일 공유 API_KEY로 지점 3개 — 노출면 확대. sensor_status는 sensor_id 단독 키이므로 Pi-C가 id 1/2 오설정 시 기존 지점 상태를 조용히 덮어씀 (php/api/sensor_data.php:316-340). 배포 전 config 교차 점검 필수.