POSA_LEAKSMS/docs/INSTALL_PI_SERVER.md
유창욱 90f121e14c chore: import codebase with security hardening
SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트).
전체 코드리뷰에서 도출된 보안 하드닝 10건 반영:
- 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시)
- 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php)
- 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점)
- CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php)
- 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화

_backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
2026-06-20 09:37:40 +09:00

255 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# STM32 펌웨어 및 Cafe24 서버 설치 설명서 v2606
이 문서는 서버실 온습도(SHT30) 모니터링 시스템 v2606을 실제 운영 환경에 배포할 때 STM32F407VGT6 펌웨어와 Cafe24 PHP/MySQL 서버에 각각 무엇을 설치하고 설정해야 하는지 정리한 설치용 문서입니다.
## 1. 설치 대상
| 구분 | 설치 위치 | 역할 |
|---|---|---|
| Cafe24 서버 | `public_html/raspi_leck_detecter/` | 센서 API 수신, DB 저장, 임계 판정, SMS 발송, 대시보드/보고서 제공 |
| Cafe24 MySQL | Cafe24 DB | 센서 로그, 온습도 측정값, SMS 로그, 장비 상태 저장 |
| STM32F407VGT6 (`sht30_fw`) | 펌웨어 플래시 | SHT30 I2C 측정, 5분 주기 보고, raw-body 서명 |
| 서버 cron | Cafe24 cron 또는 외부 스케줄러 | `cron_heartbeat.php` 주기 실행(장비 오프라인 감지) |
## 2. 설치 전 준비물
- STM32F407VGT6 보드 (STM32F4-DISCOVERY 계열 또는 동등 커스텀 PCB)
- SHT30 온습도 모듈 (한진데이터 P4422-3, I2C)
- LAN8720 Ethernet PHY 모듈 (RMII) + LAN 케이블
- I2C 풀업 저항 4.7kΩ × 2 (모듈 내장 풀업 시 생략)
- 외부 3.3V USB-UART 어댑터 (USART3 PD8/PD9 콘솔 로그용)
- Cafe24 PHP 호스팅
- Cafe24 MySQL DB
- Cafe24 SMS 서비스 계정 및 secure key
- 운영 담당자 SMS 수신 번호
- HTTPS 접속 가능한 도메인 또는 호스팅 경로 + Cafe24 루트 CA
배선은 [wiring_diagram.md](./wiring_diagram.md)를 먼저 적용합니다. SHT30 I2C(PB6 SCL / PB7 SDA, 주소 0x44, 3.3V/GND, 4.7kΩ 풀업)와 LAN8720 RMII 결선은 [firmware/docs/HARDWARE.md](../firmware/docs/HARDWARE.md)를 따릅니다.
## 3. 서버에 설치할 파일
Cafe24 웹 루트 아래에 다음 구조로 PHP 파일을 업로드합니다.
```text
public_html/raspi_leck_detecter/
.htaccess
blocked.php
config.php
config.local.php
config.local.example.php
ops_checks.php
sms_send.php
cron_heartbeat.php
dashboard.php
setup_mfa.php
admin_security.php
setup_wizard.php
security_evidence.php
monthly_report.php
login.php
retention_cleanup.php
setup_hash.php
api/
sensor_data.php
var/
.gitkeep
```
운영 서버에는 `config.local.php`가 반드시 필요합니다. `config.local.example.php`를 복사해서 만들고 실제 운영 값으로 교체합니다.
```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',
'SMS_USER_ID' => 'your-cafe24-sms-user-id',
'SMS_SECURE' => 'your-cafe24-sms-secure-key',
'SMS_SENDER' => '01000000000',
'SMS_RECIPIENTS' => [
'01000000000',
],
'ADMIN_USER' => 'admin',
'ADMIN_PASSWORD_HASH' => '$2y$10$replace.with.password_hash.output',
'ADMIN_TOTP_SECRET' => 'replace-with-base32-secret',
'MFA_SETUP_TOKEN' => 'replace-with-temporary-random-token',
// 온습도 임계 override (기본값은 config.php 의 METRIC_*)
'METRIC_TEMP_HIGH_C' => 30,
'METRIC_TEMP_LOW_C' => 10,
'METRIC_RH_HIGH' => 70,
'METRIC_RH_LOW' => 20,
'SMS_LOG_RETENTION_DAYS' => 365,
'SENSOR_LOG_RETENTION_DAYS' => 365,
'SENSOR_METRIC_RETENTION_DAYS' => 365,
'ADMIN_AUDIT_RETENTION_DAYS' => 365,
];
```
`API_KEY`는 STM32 펌웨어 `firmware/common/secrets.h``APP_API_KEY`와 반드시 바이트 단위로 같아야 합니다(raw-body 서명 `X-Signature = sha256(API_KEY + 요청본문)`).
`ADMIN_TOTP_SECRET`은 Google Authenticator, Microsoft Authenticator, Authy 등 TOTP 인증 앱에 등록한 Base32 비밀키입니다. 이 값이 없으면 관리자 로그인을 허용하지 않습니다. 담당자 변경 시 관리자 비밀번호와 TOTP 비밀키를 모두 교체하고 변경 기록을 운영 인수인계 자료에 남깁니다.
최초 등록은 `MFA_SETUP_TOKEN`을 임시로 설정한 뒤 다음 주소에서 진행합니다.
```text
https://your-domain.example/raspi_leck_detecter/setup_mfa.php?token=임시토큰
```
`setup_mfa.php`에서 생성된 수동 입력 키를 Google Authenticator에 등록하고 6자리 코드를 검증한 뒤, 화면에 표시되는 `ADMIN_TOTP_SECRET` 줄을 `config.local.php`에 반영합니다. 등록 완료 후 `MFA_SETUP_TOKEN`은 빈 값으로 바꾸거나 삭제합니다.
## 4. 서버 DB 설치
신규 설치라면 Cafe24 phpMyAdmin 또는 MySQL 콘솔에서 다음 파일을 실행합니다.
```sql
SOURCE /path/to/sql/schema_sht30.sql;
```
phpMyAdmin에서는 [schema_sht30.sql](../sql/schema_sht30.sql) 내용을 복사해서 SQL 실행 창에 붙여넣습니다. 생성 테이블은 `sensor_log`, `sensor_status`, `sensor_metric`, `sms_log`입니다.
기존 누수 설치를 온습도 전용으로 전환하는 경우에는 전환 마이그레이션을 적용합니다.
| SQL 파일 | 적용 상황 |
|---|---|
| [schema_sht30.sql](../sql/schema_sht30.sql) | 신규 설치(통합 스키마) |
| [migration_drop_leak.sql](../sql/migration_drop_leak.sql) | 기존 누수 설치 → 온습도 전용 전환(레거시 컬럼/테이블 정리, `sensor_metric` 보장) |
이미 적용된 마이그레이션을 중복 실행하면 DB 오류가 날 수 있으므로, 기존 설치에서는 `setup_wizard.php`의 점검 결과를 먼저 확인합니다.
## 5. 서버 권한 및 관리자 설정
상태 파일 폴더는 PHP가 쓸 수 있어야 합니다.
```text
php/var/
```
관리자 비밀번호 해시는 서버 CLI에서 생성합니다.
```bash
php setup_hash.php "새관리자비밀번호"
```
출력된 해시를 `config.local.php``ADMIN_PASSWORD_HASH`에 넣습니다. 관리자 인증 앱 비밀키도 `config.local.php``ADMIN_TOTP_SECRET`에 설정합니다.
서버 설정 후 브라우저에서 아래 화면을 확인합니다.
```text
https://your-domain.example/raspi_leck_detecter/setup_wizard.php
```
점검 화면에서 확인할 항목은 다음과 같습니다.
- DB 연결
- 필수 테이블 존재 여부(`sensor_log`, `sensor_status`, `sensor_metric`, `sms_log`)
- API 키 기본값 교체 여부
- 관리자 해시 설정 여부
- 관리자 TOTP 설정 여부
- SMS 수신자 설정 여부
- 상태 파일 폴더 쓰기 권한
- 테스트 SMS 발송
## 6. 서버 cron 설정
장비 오프라인 감지를 위해 `cron_heartbeat.php`를 주기 실행합니다. Cafe24 cron 또는 외부 스케줄러에서 다음 URL을 1분 간격으로 호출합니다.
```text
https://your-domain.example/raspi_leck_detecter/cron_heartbeat.php
```
서버 CLI cron을 사용할 수 있다면 다음처럼 구성할 수 있습니다.
```cron
* * * * * php /home/hosting_user/public_html/raspi_leck_detecter/cron_heartbeat.php
```
호스팅 환경마다 실제 절대 경로가 다르므로 Cafe24 파일 관리자 또는 `phpinfo()`로 경로를 확인합니다. 마지막 수신 후 `HEARTBEAT_TIMEOUT_SEC`(기본 1200초=20분)을 초과하면 오프라인으로 판정합니다.
## 7. STM32 펌웨어 빌드 (폐쇄망)
폐쇄망 빌드 머신에서 펌웨어를 빌드합니다. 의존성 벤더링과 오프라인 빌드/플래시 절차는 [firmware/docs/BUILD_OFFLINE.md](../firmware/docs/BUILD_OFFLINE.md)를 따릅니다.
```bash
cp firmware/common/secrets.h.example firmware/common/secrets.h
# secrets.h 의 APP_API_KEY 를 서버 config.local.php 의 API_KEY 와 바이트 단위로 동일하게 채운다.
```
빌드 전 확인할 설정값:
| 위치 | 설정 |
|---|---|
| `firmware/common/secrets.h` | `APP_API_KEY` = 서버 `API_KEY` |
| `firmware/common/app_config.h` | `APP_API_HOST`, `APP_API_PATH`(`/raspi_leck_detecter/api/sensor_data.php`), 네트워크(DHCP/static) |
| `firmware/common/app_config.h` | `APP_SHT30_REPORT_INTERVAL_SEC`(기본 300초), `APP_SHT30_I2C_ADDR`(0x44) |
| `firmware/certs/server_ca.c` | 자리표시자 CA → 실제 Cafe24 루트 CA |
`sht30_fw` 타깃을 빌드해 `sht30_fw.bin`을 생성하고 보드에 플래시합니다.
## 8. STM32 펌웨어 동작 확인
USART3(PD8 TX / PD9 RX, 115200 8N1)에 외부 3.3V USB-UART 어댑터를 연결해 콘솔 로그를 확인합니다.
첫 부팅 후 확인할 항목:
- I2C1 초기화 및 SHT30 주소 0x44 응답
- 첫 측정값(온도/습도)
- 네트워크(DHCP/static) → SNTP 시간 동기 → TLS 핸드셰이크
- 서버 200 응답 및 `startup` 보고
상태 LED(PD12) 패턴: 부팅=점등 / 정상 보고=느린 토글 / 망 단절·TLS 실패=빠른 점멸.
## 9. 설치 후 최종 확인
서버에서 확인합니다.
```text
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`의 필수 점검 항목이 통과
- `dashboard.php`에 최신 온도/습도와 센서 상태(`sensor_id=2`)가 표시
- `security_evidence.php`에서 운영 점검 결과와 보안통제 매트릭스 조회 가능
- STM32 콘솔 로그에 서버 200 응답 기록 확인
- `sensor_metric`에 주기 측정값(periodic) 저장
- 임계 초과 테스트 시 `metric_status` 경보 기록 및 SMS 수신
- 월간 보고서에서 임계 경보/복귀/오프라인 내역 조회 가능
## 10. 문제 해결
| 증상 | 확인 지점 |
|---|---|
| API 403 | 서버 `API_KEY`와 펌웨어 `APP_API_KEY` 일치 여부, raw-body 서명 |
| API 500 | Cafe24 PHP 오류 로그, DB 접속 정보, 테이블 생성 여부 |
| 로그인 불가 | `ADMIN_PASSWORD_HASH`, `ADMIN_TOTP_SECRET`, 인증 앱 시간 동기화 |
| SMS 미발송 | Cafe24 SMS 계정, secure key, 발신번호 등록, 잔액, `sms_log` |
| 온습도 미표시 | I2C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, `sensor_metric` 테이블, 펌웨어 측정 로그 |
| 임계 경보 미발송 | `config.php`/`config.local.php`의 `METRIC_*`, `METRIC_ALERT_COOLDOWN_SEC`(1800초) |
| TLS 실패 | `firmware/certs/server_ca.c` 실제 CA 반영, SNTP 시간 동기(인증서 유효기간 검증) |
| 오프라인 알림 과다/지연 | `HEARTBEAT_CHECK_INTERVAL_SEC`, `HEARTBEAT_TIMEOUT_SEC`(기본 1200초), cron 주기 |
## 11. 운영자가 보관해야 할 값
다음 값은 코드 저장소에 넣지 말고 운영자만 별도로 보관합니다.
- Cafe24 DB 이름, 계정, 비밀번호
- `API_KEY` / 펌웨어 `APP_API_KEY`
- Cafe24 SMS user id
- Cafe24 SMS secure key
- SMS 발신번호
- SMS 수신자 번호
- 관리자 계정과 원문 비밀번호, `ADMIN_TOTP_SECRET`
- 서버 업로드 경로
- 장비 ID(`stm32-sht30-01`)와 설치 위치