tools/make_deploy_bundles.py: deploy/ 아래 서버1+Pi3대 폴더 생성 — API_KEY 1개 자동 생성·4곳 동일 주입, 관리자 bcrypt(비번 stdin 전달), LF 정규화, 단일 schema.sql, var/ 런타임 상태 제외, CRON_SECRET 사전 계산. 난독화 경로 WEB_DIR 은 소스가 아닌 deploy/.secrets/web_dir.txt 에서 로드(커밋 트리 유출 차단). 시크릿 생성 전에 deploy/.gitignore 선작성 + 루트 .gitignore 에 deploy/ 이중 차단. tools/harden_pi.sh: SSH 완전 비활성(outbound 전용 설계) · 인바운드 전부 차단 (nftables) · 불필요 서비스 제거(avahi/bluetooth/cups 등) · 자동 보안 패치 (unattended-upgrades, 자동 리부팅 끔) · config_local.py 권한 600 · 검증 증적 출력. setup.sh(번들 생성): 설치→서비스→검증→하드닝 원샷. 검증 실패 시 set -e 로 하드닝(SSH 차단) 전에 중단 → SSH 유지되어 디버깅 가능. 검색 비노출 하드닝: php/.htaccess X-Robots-Tag noindex + Referrer-Policy no-referrer, api/photo_upload.php CORS 제거, test_mobile.php 세션 인증 가드. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821XP36Q6t9eWKTPSEumnC
91 lines
4.4 KiB
Bash
91 lines
4.4 KiB
Bash
#!/bin/bash
|
|
# =============================================================================
|
|
# harden_pi.sh — Raspberry Pi OS (Bookworm) 최소 하드닝 (센서 노드 공통)
|
|
#
|
|
# 정책 (2026-07 확정):
|
|
# - SSH 완전 비활성 — 이 시스템은 outbound 전용(Pi → 서버 HTTPS POST).
|
|
# 유지보수는 현장 물리 접근(모니터+키보드). 재활성이 필요하면 SD 카드의
|
|
# boot 파티션에 빈 파일 `ssh` 를 만들고 부팅한다(물리 접근 필요 — 의도됨).
|
|
# - 인바운드 전부 차단(nftables), 아웃바운드는 허용(apt/NTP/서버 POST).
|
|
# - 자동 보안 패치(unattended-upgrades) 켬, 자동 리부팅은 끔(센서 공백 방지).
|
|
#
|
|
# 실행: 모듈 설치(install.sh)와 검증(test_connection.py)이 전부 끝난 "마지막"에.
|
|
# sudo bash harden_pi.sh
|
|
# ⚠️ SSH 로 접속해 실행 중이면 완료 즉시 세션이 끊기고 재접속 불가 — 마지막 단계!
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
[ "$(id -u)" -eq 0 ] || { echo "[ERROR] sudo 로 실행하세요: sudo bash harden_pi.sh"; exit 1; }
|
|
|
|
echo "[1/6] SSH 완전 비활성..."
|
|
systemctl disable --now ssh.service ssh.socket 2>/dev/null || true
|
|
# boot 파티션 `ssh` 파일에 의한 재활성 경로(sshswitch)는 남겨둔다 — 물리 접근
|
|
# 없이는 사용할 수 없고, 현장 복구 수단으로 유용하다.
|
|
|
|
echo "[2/6] 불필요 리스닝/주변 서비스 비활성..."
|
|
for svc in avahi-daemon.service avahi-daemon.socket bluetooth.service hciuart.service \
|
|
triggerhappy.service triggerhappy.socket cups.service cups.socket \
|
|
cups-browsed.service ModemManager.service; do
|
|
systemctl disable --now "$svc" 2>/dev/null || true
|
|
done
|
|
|
|
echo "[3/6] 방화벽(nftables) — 인바운드 전부 차단..."
|
|
apt-get install -y -q nftables >/dev/null
|
|
cat > /etc/nftables.conf <<'NFT'
|
|
#!/usr/sbin/nft -f
|
|
# 센서 노드 방화벽 — outbound 전용 장비. 인바운드는 루프백/응답 트래픽만.
|
|
flush ruleset
|
|
table inet filter {
|
|
chain input {
|
|
type filter hook input priority 0; policy drop;
|
|
iif "lo" accept
|
|
ct state established,related accept
|
|
# 현장 진단용 ping 허용 (원치 않으면 아래 2줄 삭제)
|
|
ip protocol icmp icmp type echo-request accept
|
|
ip6 nexthdr ipv6-icmp accept
|
|
}
|
|
chain forward { type filter hook forward priority 0; policy drop; }
|
|
chain output { type filter hook output priority 0; policy accept; }
|
|
}
|
|
NFT
|
|
systemctl enable --now nftables
|
|
nft -f /etc/nftables.conf
|
|
|
|
echo "[4/6] 자동 보안 패치(unattended-upgrades)..."
|
|
apt-get install -y -q unattended-upgrades >/dev/null
|
|
# Raspberry Pi OS 는 기본 Origins 패턴(Debian 전용)에 안 걸린다 — Raspbian/RPF origin 추가.
|
|
cat > /etc/apt/apt.conf.d/51unattended-upgrades-raspi <<'APT'
|
|
Unattended-Upgrade::Origins-Pattern {
|
|
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
|
|
"origin=Raspbian,codename=${distro_codename},label=Raspbian";
|
|
"origin=Raspberry Pi Foundation,codename=${distro_codename},label=Raspberry Pi Foundation";
|
|
};
|
|
// 자동 리부팅 금지 — 커널 패치 후에도 센서 공백을 만들지 않는다(수동 리부팅).
|
|
Unattended-Upgrade::Automatic-Reboot "false";
|
|
APT
|
|
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'APT'
|
|
APT::Periodic::Update-Package-Lists "1";
|
|
APT::Periodic::Unattended-Upgrade "1";
|
|
APT
|
|
systemctl enable --now unattended-upgrades 2>/dev/null || true
|
|
|
|
echo "[5/6] 로컬 권한 조이기..."
|
|
# 시크릿 파일 소유자 전용 (API_KEY 포함)
|
|
for f in /home/pi/raspberry_pi4/config_local.py /home/pi/raspberry_pi4_sht30/config_local.py; do
|
|
[ -f "$f" ] && { chmod 600 "$f"; chown pi:pi "$f"; echo " 600: $f"; }
|
|
done
|
|
# 콘솔 자동 로그인 비활성 (물리 콘솔도 로그인 요구)
|
|
if command -v raspi-config >/dev/null 2>&1; then
|
|
raspi-config nonint do_boot_behaviour B1 || true
|
|
fi
|
|
|
|
echo "[6/6] 검증 (증적 캡처 대상 — SOURCE_SECURITY_EVIDENCE.md §4)..."
|
|
echo "-- ssh 상태 (disabled 여야 함):"
|
|
systemctl is-enabled ssh.service 2>&1 || true
|
|
echo "-- 리스닝 소켓 (비어 있거나 로컬 전용이어야 함):"
|
|
ss -lntup || true
|
|
echo "-- nftables 룰 (input policy drop 확인):"
|
|
nft list chain inet filter input | head -12
|
|
echo ""
|
|
echo "완료. 위 출력을 캡처해 보안 증적으로 보관할 것."
|
|
echo "리부팅 권장: sudo reboot (이후 ss -lntup 재확인)"
|