#!/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 재확인)"