서버실 온습도(SHT30) 모니터링 시스템 v2606 — 설치 가이드

Cafe24 PHP/MySQL 서버부터 STM32F407 MCU 펌웨어까지, 폐쇄망(air-gapped) 배포 절차

서버: Cafe24 PHP / MySQL MCU: STM32F407VGT6 (sht30_fw) 센서: SHT30 I²C 0x44 전송: HMAC-SHA256 + TLS 1.2 폐쇄망 · 인터넷 없음
이 가이드의 이미지에 대하여. 본 문서의 아키텍처·배선 다이어그램은 코드/HARDWARE.md 기준으로 정확히 생성한 SVG입니다. Cafe24 관리자 콘솔·phpMyAdmin·플래싱 툴·실제 대시보드 화면처럼 운영 환경에서만 캡처할 수 있는 화면은 “📷 캡처 위치” 자리표시로 두었습니다. 각 자리표시의 캡션이 가리키는 화면을 직접 캡처해 그 자리에 넣으면 됩니다. (저장소의 기존 캡처는 구버전 v2605 누수감지 UI라 현재 화면과 다릅니다 — 부록 참조.)

1. 시스템 개요 & 아키텍처

단일 STM32F407 보드(sht30_fw, sensor_id=2)가 SHT30 센서를 I²C로 5분마다 읽어, 본문을 HMAC-SHA256으로 서명해 TLS 1.2로 Cafe24 PHP 서버에 POST합니다. 서버가 임계를 재판정하여 고온/저온/고습/저습·결로·오프라인 상황을 SMS로 알리고 웹 대시보드/보고서를 제공합니다.

SHT30온·습도 센서 STM32F407sht30_fwFreeRTOS+mbedTLS Cafe24 PHPapi/sensor_data.php임계 재판정 + 저장 MySQL4 테이블 SMS · 대시보드경보/보고서 I²C 0x44 HMACTLS 1.2
그림 1. 전체 데이터 흐름 — SHT30 → STM32(서명/TLS) → Cafe24(재판정/저장) → MySQL + SMS/대시보드. (코드 기준 생성)
구분설치 위치역할
Cafe24 서버public_html/raspi_leck_detecter/센서 API 수신, DB 저장, 임계 판정, SMS, 대시보드/보고서
Cafe24 MySQLCafe24 DB센서 로그 · 온습도 측정값 · SMS 로그 · 장비 상태
STM32F407VGT6펌웨어 플래시 (sht30_fw)SHT30 I²C 측정, 5분 주기 보고, raw-body HMAC 서명
서버 cronCafe24 cron 또는 외부 스케줄러cron_heartbeat.php 주기 실행(오프라인 감지·헬스·에스컬레이션)
웹 루트 경로 고정. 폴더명 raspi_leck_detecter 는 레거시 이름이지만 펌웨어 app_config.h 의 APP_API_PATH 와 반드시 일치해야 합니다. 폴더명을 바꾸려면 펌웨어도 함께 바꿔 재빌드해야 합니다.

2. 설치 전 준비물

서버측(Cafe24)장치측(STM32)
• Cafe24 PHP 호스팅
• Cafe24 MySQL DB(이름/계정/비밀번호)
• Cafe24 SMS 서비스 계정 + secure key + 발신번호
• 운영 담당자 SMS 수신번호
• HTTPS 도메인/경로 + Cafe24 루트 CA
• STM32F407VGT6 보드(DISCOVERY 계열/커스텀 PCB)
• SHT30 모듈(한진데이터 P4422-3, I²C)
• LAN8720 Ethernet PHY(RMII) + LAN 케이블
• I²C 풀업 4.7kΩ × 2 (모듈 내장 시 생략)
• 외부 3.3V USB-UART 어댑터(USART3 콘솔)
• 폐쇄망 빌드 머신(arm-none-eabi-gcc, CMake, Ninja, ST-Link 툴)
핵심 비밀값 한 쌍. 서버 config.local.php 의 API_KEY 와 펌웨어 secrets.h 의 APP_API_KEY 는 바이트 단위로 동일해야 합니다. 서명 X-Signature = lowercase_hex(HMAC-SHA256(key=API_KEY, msg=요청본문)) 검증의 기준입니다.
PART A · Cafe24 서버

3. 서버 파일 업로드

Cafe24 FTP(또는 파일 관리자)로 웹 루트 아래에 PHP 트리를 업로드합니다.

public_html/raspi_leck_detecter/
  .htaccess              config.php            config.local.php   ← 서버에서만 생성
  config.local.example.php
  login.php              dashboard.php         setup_wizard.php
  setup_mfa.php          admin_security.php    ops_checks.php
  sms_send.php           cron_heartbeat.php    monthly_report.php
  security_evidence.php  retention_cleanup.php setup_hash.php
  blocked.php            test_mobile.php
  api/
    sensor_data.php
  var/
    .gitkeep             ← PHP가 쓰기 가능해야 함(상태/감사/카운터 파일)
업로드 금지. config.local.php 와 펌웨어 secrets.h 는 비밀값이므로 저장소/공유에 올리지 않습니다. php/var/ 는 .htaccess 로 외부 접근이 차단됩니다(확인 필수).
그림 2 캡처
그림 2. 서버 파일 업로드 결과(웹 루트 트리)
PART A · Cafe24 서버

4. 운영 설정 — config.local.php

config.local.example.php 를 복사해 config.local.php 를 만들고 실제 운영 값으로 교체합니다.

<?php
return [
    'DB_HOST' => 'localhost',
    'DB_PORT' => 3306,
    'DB_NAME' => 'your_db_name',
    'DB_USER' => 'your_db_user',
    'DB_PASS' => 'your_db_password',

    'API_KEY' => 'replace-with-long-random-secret',   // 펌웨어 APP_API_KEY 와 동일!
    'ALLOW_LEGACY_BODY_SIGNATURE' => false,            // HMAC 전용(레거시 RPi 없으면 false)

    'SMS_USER_ID' => 'your-cafe24-sms-user-id',
    'SMS_SECURE'  => 'your-cafe24-sms-secure-key',
    'SMS_SENDER'  => '01000000000',
    'SMS_RECIPIENTS' => ['01000000000'],
    'SMS_RECIPIENTS_TIER2' => [],                      // 미복귀 30분 시 2차(야간) 통지(opt-in)

    'ADMIN_USER' => 'admin',
    'ADMIN_PASSWORD_HASH' => '',   // setup_hash.php 출력으로 채움(빈값=로그인 차단)
    'ADMIN_TOTP_SECRET'   => '',   // setup_mfa.php 등록 후 채움(빈값=로그인 차단)
    'MFA_SETUP_TOKEN'     => 'replace-with-temporary-random-token',  // 등록 후 비울 것

    // 온습도 임계(서버 판정 → SMS). 기본값은 config.php 의 METRIC_*
    'METRIC_TEMP_HIGH_C' => 30, 'METRIC_TEMP_LOW_C' => 10,
    'METRIC_RH_HIGH' => 70,      'METRIC_RH_LOW' => 20,
];
fail-closed 기본값. ADMIN_PASSWORD_HASH 또는 ADMIN_TOTP_SECRET 가 비어 있으면 관리자 로그인이 차단됩니다(보안). 4·6단계에서 채웁니다.
PART A · Cafe24 서버

5. 데이터베이스 설치 (phpMyAdmin)

Cafe24 phpMyAdmin 접속 → 대상 DB 선택

Cafe24 호스팅 관리 → MySQL → phpMyAdmin. 좌측에서 운영 DB(DB_NAME)를 클릭합니다.

스키마 실행

신규 설치는 sql/schema_sht30.sql 전체를 복사해 SQL 탭에 붙여넣고 실행. 생성 테이블: sensor_log, sensor_status, sensor_metric, sms_log (+ 재전송 방어용 ingest_nonce).

SQL 파일적용 상황
sql/schema_sht30.sql신규 설치(통합 스키마)
sql/migration_drop_leak.sql기존 누수(v2605) 설치 → 온습도 전용 전환. 비가역 — 실행 전 DB 전체 백업
중복 실행 주의. 이미 적용한 마이그레이션을 다시 실행하면 오류가 날 수 있습니다. 기존 설치는 setup_wizard.php 점검 결과를 먼저 확인하세요.
그림 3 캡처
그림 3. DB 스키마 실행 결과 (sensor_log/sensor_status/sensor_metric/sms_log/ingest_nonce)
PART A · Cafe24 서버

6. 권한 · 관리자 비밀번호 · MFA 등록

상태 파일 폴더 쓰기 권한

raspi_leck_detecter/var/ 가 PHP에서 쓰기 가능해야 합니다(브루트포스/하트비트/감사로그/거부 카운터 파일 저장).

관리자 비밀번호 해시 생성(서버 CLI)

php setup_hash.php "새관리자비밀번호"

출력된 bcrypt 해시를 config.local.php 의 ADMIN_PASSWORD_HASH 에 넣습니다.

MFA(TOTP) 등록

MFA_SETUP_TOKEN 을 임시 설정한 뒤 아래 주소로 접속해 인증 앱(Google/Microsoft Authenticator, Authy)에 등록합니다.

https://your-domain.example/raspi_leck_detecter/setup_mfa.php?token=임시토큰

화면의 Base32 키를 인증 앱에 등록 → 6자리 코드 검증 → 표시된 ADMIN_TOTP_SECRET 줄을 config.local.php 에 반영 → 등록 후 MFA_SETUP_TOKEN 을 빈 값으로 바꿉니다.

그림 4 캡처
그림 4. 관리자 MFA(TOTP) 등록 화면

설치 점검 화면 확인

https://your-domain.example/raspi_leck_detecter/setup_wizard.php

확인 항목: DB 연결 · 필수 테이블 · API 키 교체 · 관리자 해시 · 관리자 TOTP · SMS 수신자 · var/ 쓰기 권한 · 테스트 SMS 발송.

그림 5 캡처
그림 5. 설치 점검(setup_wizard) 통과 화면
PART A · Cafe24 서버

7. cron 설정 — 오프라인 감지 · 헬스 · 에스컬레이션

cron_heartbeat.php 를 1분 간격으로 실행합니다(오프라인 감지 + 일일 헬스 SMS + 미복귀 에스컬레이션).

방법 A — 서버 CLI cron

* * * * * php /home/hosting_user/public_html/raspi_leck_detecter/cron_heartbeat.php

방법 B — HTTP 호출(외부 스케줄러)

https://your-domain.example/raspi_leck_detecter/cron_heartbeat.php?key=<CRON_SECRET>

CRON_SECRET = HMAC-SHA256("cron", API_KEY) 입니다(코드가 파생). 절대 경로는 Cafe24 파일 관리자/phpinfo()로 확인. 마지막 수신 후 HEARTBEAT_TIMEOUT_SEC(기본 1200초=20분) 초과 시 오프라인 판정.

cron이 없으면? 다른 페이지(ingest/대시보드) 접속 시 5분 쓰로틀로 piggyback 실행됩니다. 단, 완전 유휴 상태에선 오프라인 감지가 늦어질 수 있으니 cron 등록을 권장합니다.
그림 6 캡처
그림 6. Cafe24 cron(예약작업) 등록
PART B · STM32 MCU

8. 하드웨어 배선 (STM32F407VGT6)

LQFP-100 기준. SHT30 I²C, LAN8720 RMII, USART3 콘솔, 상태 LED를 결선합니다.

STM32F407VGT6 168MHz · sht30_fw HSE 8MHz→PLL · IWDG 20s · LSE RTC SHT30P4422-3 · I²C 0x44 VDDSCLSDAGND 3.3V PB6 SCL PB7 SDA GND 4.7kΩ↑3.3V (SCL/SDA) LAN8720 PHY (RMII · AF11) PA1 REF_CLK · PA2 MDIO · PC1 MDC · PA7 CRS_DV PC4/PC5 RXD0/1 · PB11 TXEN · PB12/PB13 TXD0/1 MDIO 1.5kΩ↑3.3V · 50MHz REF_CLK 콘솔 & 상태 LED USART3: PD8 TX / PD9 RX (AF7, 115200 8N1) 외부 3.3V USB-UART · 상태 LED = PD12 ※ PB6/PB7(I²C)와 PB11/12/13(RMII)은 같은 PB포트지만 충돌 없음. ※ PA2는 ETH_MDIO 전용 — USART2 사용 금지(콘솔은 USART3 PD8/PD9).
그림 7. STM32F407 + SHT30 + LAN8720 + USART3 콘솔 배선 (HARDWARE.md 핀맵 기준 생성)

8.1 SHT30 I²C1

SHT30STM32 핀비고
VDD3.3VSCL/SDA 각각 4.7kΩ → 3.3V 풀업(모듈 내장 시 생략). 100kHz 권장. 명령 0x2C06 → ~20ms → 6바이트 read
SCLPB6
SDAPB7
GND / ADDRGND

8.2 LAN8720 RMII (AF11)

STM32 핀RMII 신호STM32 핀RMII 신호
PA1ETH_REF_CLK (50MHz 입력)PC4ETH_RXD0
PA2ETH_MDIO ⚠️ (USART2 금지)PC5ETH_RXD1
PC1ETH_MDCPB11ETH_TX_EN
PA7ETH_CRS_DVPB12/PB13ETH_TXD0 / TXD1

8.3 콘솔 · 상태 LED · 클럭

PART B · STM32 MCU

9. 폐쇄망 펌웨어 빌드

폐쇄망 원칙. 다운로드는 인터넷 가능한 빌드 머신에서만 일어나고, 결과물 .bin 이 에어갭을 넘어 전달됩니다. 설치 호스트는 어떤 것도 직접 가져오지 않습니다.

의존성 벤더링(빌드 머신, 외부 다운로드는 여기서만)

bash firmware/scripts/vendor.sh        # 또는 vendor.ps1
# 릴리스 빌드는 핀(SHA-256) 강제:
VENDOR_STRICT=1 bash firmware/scripts/vendor.sh

STM32CubeF4 HAL/CMSIS · FreeRTOS-Kernel · lwip(+sntp) · mbedtls 를 firmware/third_party/ 에 고정 버전으로 배치. 빌드·런타임 외부 fetch 0건.

비밀값 — secrets.h

cp firmware/common/secrets.h.example firmware/common/secrets.h
# APP_API_KEY 를 서버 config.local.php 의 API_KEY 와 바이트 단위로 동일하게 채운다.
secrets.h 는 커밋 금지. 산출 .bin 에 키가 임베드되므로 .bin 자체를 비밀로 취급합니다.

서버 루트 CA 임베드 — certs/server_ca.c (필수)

openssl s_client -connect your-domain.example:443 -servername your-domain.example -showcerts </dev/null
#   → 루트 CA 를 PEM 으로 저장 후 certs/server_ca.c 의 SERVER_CA_PEM 에 반영
#   (SERVER_CA_PEM_LEN = sizeof(...) , 종결 NUL 포함)
자리표시자 CA로는 부팅 불가. 기본 server_ca.c 는 PLACEHOLDER라 tls_init()가 실패해 부팅이 정지(fail-closed)합니다. build.ps1 이 PLACEHOLDER를 감지하면 빌드를 중단합니다 — 반드시 실제 CA로 교체하세요.

설정값 확인 — app_config.h

설정값
APP_API_HOST / APP_API_PATH운영 도메인 / /raspi_leck_detecter/api/sensor_data.php
APP_SNTP_SERVER ⚠️폐쇄망 LAN NTP 서버 IP(기본 192.0.2.123 자리표시자 — 반드시 교체)
APP_SHT30_I2C_ADDR / APP_SHT30_REPORT_INTERVAL_SEC0x44 / 300(5분)
SNTP는 폐쇄망 전면장애 지점. 인터넷 NTP(pool.ntp.org)는 폐쇄망에서 도달 불가 → 시간 미동기 → timestamp=0 → TLS 차단 + 서버 전량 거부. 반드시 사내 LAN NTP IP로 설정하세요.

빌드 (cmake + ninja)

cmake -S firmware -B firmware/build -G Ninja \
      -DCMAKE_TOOLCHAIN_FILE=firmware/cmake/arm-none-eabi-toolchain.cmake
cmake --build firmware/build --target sht30_fw   # → sht30_fw.elf / .bin / .hex

(선택) 호스트 패리티 게이트로 C↔Python↔PHP 서명 일치 확인: python3 firmware/test/host/parity_test.py

그림 8 캡처
그림 8. 펌웨어 빌드 성공 로그
PART B · STM32 MCU

10. 에어갭 전달 · 플래시 · 콘솔 확인

무결성 해시 + 신뢰 매체 전달

sha256sum firmware/build/sht30_fw.bin > FW_HASHES.txt   # 빌드 머신
# USB 등 신뢰 매체로 격리 호스트에 전달 후 재계산 대조

ST-Link 플래시 (격리 호스트)

pwsh firmware/scripts/flash.ps1                 # 권장(자동 탐지)
# 대안:
st-flash write sht30_fw.bin 0x08000000
STM32_Programmer_CLI -c port=SWD -w sht30_fw.hex -v -rst
그림 9 캡처
그림 9. STM32 펌웨어 플래시 완료

콘솔 로그 확인 (USART3 PD8/PD9, 115200 8N1)

외부 USB-UART 어댑터를 연결하고 부팅 시퀀스를 관찰합니다:

bsp_init → 링크업 → DHCP(또는 static) → SNTP 시간 동기
        → TLS 핸드셰이크 → 첫 startup POST → 서버 HTTP 200

I²C1 0x44 응답 · 첫 온/습도 측정 · 상태 LED(PD12) 패턴을 함께 확인. 서명 불일치(403)면 API_KEY↔APP_API_KEY, TLS 실패면 server_ca.c/SNTP를 재점검.

그림 10 캡처
그림 10. STM32 콘솔 부팅 로그(정상 보고)

11. 최종 검증 (종단)

서버에서 아래 화면을 확인합니다.

https://your-domain.example/raspi_leck_detecter/setup_wizard.php
https://your-domain.example/raspi_leck_detecter/dashboard.php
https://your-domain.example/raspi_leck_detecter/monthly_report.php
https://your-domain.example/raspi_leck_detecter/security_evidence.php
정상 설치 기준
  • setup_wizard.php 필수 점검 항목 통과 + 테스트 SMS 수신
  • dashboard.php 에 최신 온/습도와 센서 상태(sensor_id=2) 표시
  • sensor_metric 에 주기 측정값(periodic) 저장 · STM32 콘솔에 서버 200 기록
  • 임계 초과 테스트 시 metric_status 경보 + SMS 수신 / 월간 보고서에 내역 조회
그림 11 캡처
그림 11. 운영 대시보드(v2606 온습도) — 데스크톱 / 모바일

12. 문제 해결

증상확인 지점
API 403서버 API_KEY ↔ 펌웨어 APP_API_KEY 일치, raw-body 서명
API 500Cafe24 PHP 오류 로그, DB 접속 정보, 테이블 생성 여부
로그인 불가ADMIN_PASSWORD_HASH, ADMIN_TOTP_SECRET, 인증 앱 시간 동기
SMS 미발송Cafe24 SMS 계정·secure key·발신번호 등록·잔액, sms_log. 대시보드 ‘수집 거부/발송 실패’ 배너 확인
온습도 미표시I²C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, sensor_metric, 펌웨어 측정 로그
TLS 실패server_ca.c 실제 CA 반영, SNTP 시간 동기(APP_SNTP_SERVER=LAN NTP)
데이터 전혀 안 들어옴대시보드 수집 거부 사유 카운터(서명불일치→키/펌웨어, 시각오차→SNTP/RTC)로 원인 분별
오프라인 알림 과다/지연HEARTBEAT_TIMEOUT_SEC(1200초), cron 주기

13. 운영자가 별도 보관할 값

아래 값은 저장소에 넣지 말고 운영자만 안전하게 보관합니다.

부록 — 저장소 내 기존 이미지 자산 (레거시 · 참고용)

주의: 아래 캡처는 구버전 v2605 누수감지(카메라/사진) UI 또는 Raspberry Pi 기준이라 현재 v2606 STM32/온습도 시스템 화면과 다릅니다. 화면 구성 참고용으로만 사용하고, 위 본문의 “📷 캡처 위치”에는 현재 운영 시스템에서 직접 캡처한 이미지를 넣으세요.

본 HTML 파일은 docs/ 폴더 기준 상대경로로 아래 자산을 참조할 수 있습니다(폴더 동반 시 표시됨):

경로내용상태
examples/captures/admin_dashboard_overview.png관리자 대시보드 개요⚠️ v2605 누수 UI
examples/captures/admin_dashboard_mobile.png대시보드 모바일⚠️ v2605
examples/captures/admin_dashboard_incident.png경보/사고 카드⚠️ v2605(사고/사진)
evidence/mfa_setup_page_capture.pngMFA 등록 페이지참고(개념 동일)
examples/assets/sht30_sensor2_wiring.svg배선도⚠️ Raspberry Pi 기준
assets/security_plan/generated/security_plan_target_architecture.png목표 아키텍처보안계획 다이어그램

현재 시스템의 정확한 아키텍처·배선은 본문 그림 1·7(생성 SVG)을 기준으로 하세요.

서버실 온습도(SHT30) 모니터링 시스템 v2606 · 설치 가이드 · 폐쇄망(air-gapped) 배포 · 인쇄(Ctrl/Cmd+P) 지원