- 전 운영 문서 테이블명 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
28 KiB
배포 계획: 누수+온습도 통합 모니터링 시스템 (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.sql1회 실행. 신규 설치는 이 파일 하나로 완결 — 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.pyAPI_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, commitc8b448c)
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 --executeCLI 전용 (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.pyUSE_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.phpPHOTO_UPLOAD_URL=.../api/photo_upload.phpAPI_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.pyI2C_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/.htaccessFilesMatch —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 — 배포를 잘못되게 만들 수 있는 것
- 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 갱신 필요. docs/INSTALL_PI_SERVER.md업로드 목록 STALE: photo_upload.php/photo.php/var 가드 누락 (33-56행) — 그대로 따르면 누수 사진 파이프라인 결손. 또한 이 문서 자체가 STM32(v2606) 중심이며 RPi4 신규 설치 통합 가이드는 부재 (root README.md는 0바이트).- cron URL 오류: INSTALL_PI_SERVER.md:165에
?key=<CRON_SECRET>누락 → 403. CRON_SECRET 표시 UI 없음, 수동 계산 필요. - 신품 Pi 프로비저닝 절차 전무: SD 이미징,
pi계정 생성(Bookworm 기본 미존재), SSH 활성화, 호스트네임, 네트워크 설정 — 전부 미문서화. 결정 필요: 이미지 버전, 계정/비밀번호 정책, 고정IP vs DHCP, 호스트네임 규약. - Pi-C 센서 종류 결정: §6.3 비교 기준으로 결정. 누수형 선택 시 HEARTBEAT_TIMEOUT_SEC 충돌(전역 1200초 vs 보고 3600초)을 반드시 먼저 해결.
P1 — 운영 중 사고로 이어질 수 있는 것
- Pi OS 하드닝 부재: 계정/SSH/방화벽/시크릿 파일 권한(600)/패치 정책이 4개 보안 문서 어디에도 없음 —
docs/SOURCE_SECURITY_EVIDENCE.md도 "코드로 증명 불가, 기관 자료 필요"로만 표기. → 자체 표준 수립 필요. - 단일 공유 API_KEY / 센서 레지스트리 부재: 키 보유자는 임의 sensor_id 생성·기존 id 덮어쓰기 가능; 기기별 폐기 불가. 지점이 늘수록(Pi-C) 위험 증가 — device_id↔sensor_id 서버측 바인딩 검토 권장.
- 시뮬레이션 폴백이 오류 은폐: SHT30 모듈은 I2C 실패 시 그럴듯한 더미값을 운영 서버로 전송(WARN 로그만) — FORCE_REAL 옵션 없음. 배포 검증 시 journalctl 확인을 절차화할 것.
- NTP 의존 미절차화: 서버가 ±600초 강제하는데 시각동기 확인 단계가 설치 절차에 없음 (현장 인터넷 가용이라 기본 NTP로 충족 예상되나 확인 단계 추가 권장).
- 카메라 검증 단계 부재 (Pi-A): 첫 누수 때에야 실패 발견 — 설치 시 테스트샷 절차 추가 필요.
P2 — 정리/개선
- 설정 규약 이원화:
docs/SHT30_SENSOR2_SETUP.md§3과php/ops_checks.php:97은/etc/sht30-sensor.env를 참조하나 실제 모듈은config_local.py만 읽음 — 문서 갱신 필요. - SENSOR_ID가 추적 파일(config.py)에 위치 (SHT30형): config_local 오버라이드 허용하는 소규모 수정 권장 (Pi-C에서 실익).
- systemd 유닛 경로/User=pi 하드코딩 2개 파일 — 비표준 배치 시 수동 수정 2곳.
- 센서 폐기 UI 없음: leak_sensor_status 행 수동 SQL 삭제 전까지 영구 오프라인 표시.
- 임계값 문서 드리프트: SOURCE_SECURITY_EVIDENCE.md §2.4(30℃/70%)와 현행 권장값(27℃/60%, commit
c8b448c) 불일치. - retention_cleanup cron 미문서화; 오프라인 번들 해시 검증 부재(--require-hashes 없음); CORS pores.kr 잔재; 로그 권한 실패 시 조용한 콘솔 폴백(양 모듈); var/ 권한 비트 미문서화(cafe24에서 setup_wizard '상태 파일 폴더' 체크로 확인).
- 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:13SENSORS 리스트에 id=3 항목 추가 형태라 구조는 명확.
9.2 필수 코드 작업 (검증 완료 — file:line 확인됨)
- 하트비트 오탐 (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_INTERVAL3600 → 600 (Pi-A·Pi-C 공통). 주기 POST는 소형 JSON이라 부하 무시 가능. 서버 무수정. - (b) 구조적: sensor_status에 per-sensor timeout 컬럼 추가 + cron_heartbeat/dashboard 반영 (스키마+코드 변경).
- (a) 최소 변경: 누수 모듈
- 대시보드 누수 카드 (Pi-C 가시성)
php/dashboard.php:260—(int)$s['sensor_id'] === 1하드코딩. Pi-C 누수 상태 카드 미표시.- 수정: is_leak 값이 있는 모든 센서에 대해 카드 렌더 (사고 테이블은 이미 무필터 — 수정 불요).
- Pi-C 설정값
raspberry_pi4/config.pySENSORS에{"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 교차 점검 필수.