From de51d1c3d45714678f356aafbda26196db4e3fcc Mon Sep 17 00:00:00 2001 From: cw2007 Date: Fri, 3 Jul 2026 09:28:44 +0900 Subject: [PATCH] =?UTF-8?q?fix(security):=20Pi=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=ED=95=98=EB=93=9C=EB=8B=9D=206=EC=A2=85=20medium=20=EA=B0=AD?= =?UTF-8?q?=20=EB=B3=B4=EC=99=84=20+=20=ED=9A=8C=EA=B7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 적대적 보안 감사에서 확정된 medium 6건 보완 (critical/high 0): - systemd 유닛(양 노드): NoNewPrivileges + ProtectSystem=strict/ ProtectHome=read-only/ProtectKernel*/ProtectControlGroups/RestrictSUIDSGID/ LockPersonality/RestrictRealtime/RestrictAddressFamilies(+AF_NETLINK) 샌드박싱. - harden_pi.sh: SSH 차단을 마지막 단계([6/6])로 재정렬 — 앞 단계 실패 시 set -e 로 SSH 가 살아있는 상태에서 중단(원격 복구 가능). nftables·unattended-upgrades 를 오프라인(debs/ --no-download)으로도 설치 지원(폐쇄망 harden). 미발견 secret 경고. - install.sh(양 노드): config_local.py 설치 즉시 chmod 600 (harden 전 노출창 제거). - vendor.sh(양 노드): .deb 를 파일명이 아닌 dpkg-deb Architecture 필드로 실검사 (arm64/all 만 허용, 그 외 hard-fail). nftables·unattended-upgrades 를 번들에 포함. 검증(3-리뷰어 워크플로)이 잡은 회귀 동반 수정: - [critical] ProtectHome=read-only 가 lgpio 의 작업디렉터리(.lgd-nfy 파이프) 쓰기를 막아 add_event_detect(누수 인터럽트 등록) 실패 → 데몬 크래시루프 → 감지 불능. Environment=LG_WD=/tmp 로 PrivateTmp 쓰기가능 /tmp 이동(누수 노드 한정, SHT30 무관). - [medium] 오프라인 설치에 --no-install-recommends 추가(vendor --no-recommends 정합, 네트워크 폴백 방지) — install.sh x2 + harden_pi.sh. - SupplementaryGroups=video gpio 명시 · nft|head 에 ||true(SIGPIPE 중단 방지) · online harden 분기에 apt-get update. ※ 온디바이스 런타임 테스트 1회 필요(빌드머신 Windows 에선 systemd 실행 불가): 실 Pi 에서 systemctl start 후 add_event_detect 콜백·카메라 촬영·업로드 정상 확인. --- raspberry_pi4/install.sh | 12 ++- raspberry_pi4/systemd/leak-sensor.service | 31 +++++- raspberry_pi4/vendor.sh | 22 ++++- raspberry_pi4_sht30/install.sh | 12 ++- .../systemd/sht30-sensor.service | 20 ++++ raspberry_pi4_sht30/vendor.sh | 22 ++++- tools/harden_pi.sh | 99 ++++++++++++++----- 7 files changed, 189 insertions(+), 29 deletions(-) diff --git a/raspberry_pi4/install.sh b/raspberry_pi4/install.sh index e4c22e7..54fbe1a 100644 --- a/raspberry_pi4/install.sh +++ b/raspberry_pi4/install.sh @@ -50,7 +50,8 @@ if [ "${OFFLINE}" = "1" ]; then exit 1 fi echo " 오프라인 .deb 설치 (apt-get --no-download — 의존성 순서·해결 자동, 네트워크 미사용)" - sudo apt-get install -y --no-download ./debs/*.deb + # --no-install-recommends: vendor.sh 가 Recommends 를 안 담으므로(--no-recommends) 맞춘다. + sudo apt-get install -y --no-install-recommends --no-download ./debs/*.deb else echo " 온라인 apt 설치 (인터넷 필요)" sudo apt-get update @@ -91,6 +92,15 @@ if ! sudo chown "${SERVICE_USER}:${SERVICE_USER}" /var/log/leak_sensor.log; then fi sudo chmod 640 /var/log/leak_sensor.log +# 3.5) config_local.py 즉시 잠금 (API_KEY 포함 — harden 전까지 world-readable 방지) +# 하드닝(harden_pi.sh)이 나중에 다시 조이지만, 그 사이 창(window)에도 노출되지 +# 않도록 설치 시점에 바로 600 으로 잠근다. harden 여부와 무관하게 보호된다. +if [ -f config_local.py ]; then + chmod 600 config_local.py + sudo chown "${SERVICE_USER}:${SERVICE_USER}" config_local.py 2>/dev/null || true + echo " config_local.py 권한 600 (소유자 ${SERVICE_USER}) 설정" +fi + # 4) config_local 안내 (fail closed — 없으면 실행 시 즉시 중단됨) if [ ! -f config_local.py ]; then echo "" diff --git a/raspberry_pi4/systemd/leak-sensor.service b/raspberry_pi4/systemd/leak-sensor.service index 30eaa91..99ffdbe 100644 --- a/raspberry_pi4/systemd/leak-sensor.service +++ b/raspberry_pi4/systemd/leak-sensor.service @@ -22,7 +22,8 @@ Wants=network-online.target Type=simple User=pi Group=pi -SupplementaryGroups=video +# rpi-lgpio 는 /dev/gpiochip0(root:gpio 660) 접근에 gpio 그룹, 카메라는 video 그룹 필요. +SupplementaryGroups=video gpio WorkingDirectory=/home/pi/raspberry_pi4 ExecStart=/home/pi/raspberry_pi4/venv/bin/python leak_sensor.py Restart=always @@ -33,10 +34,38 @@ SyslogIdentifier=leak-sensor # 환경 변수 Environment=PYTHONUNBUFFERED=1 +# lgpio(rpi-lgpio 백엔드)는 add_event_detect 시 작업디렉터리에 .lgd-nfy 알림 파이프를 +# 만든다. 아래 ProtectHome=read-only 로 /home 이 읽기전용이 되므로, PrivateTmp 의 +# 쓰기가능 /tmp 로 lgpio 작업디렉터리를 옮긴다. (없으면 누수 인터럽트 등록이 실패해 +# 데몬이 크래시루프 → 누수 감지 불능.) LG_WD 는 lg 라이브러리가 읽는 환경변수. +# ※ 이 하드닝은 chardev 백엔드(python3-rpi-lgpio) 를 가정한다. sysfs 기반 +# python3-rpi.gpio 로 되돌리면 ProtectKernelTunables/ProtectHome 와 충돌하므로 +# 백엔드를 바꾸지 말 것(vendor.sh/install.sh 의 python3-rpi-lgpio 핀 유지). +Environment=LG_WD=/tmp # 누수 촬영 임시파일을 프로세스 전용 /tmp 로 격리 (공용 /tmp 노출 방지) PrivateTmp=true +# ── 샌드박싱(하드닝) — 데몬 침해 시 root 승격·파일시스템 오염을 차단 ────────── +# User=pi 는 NOPASSWD sudo 계정이므로 NoNewPrivileges 로 setuid(sudo) 승격 경로를 막는다. +NoNewPrivileges=true +# 루트파일시스템 읽기전용 + /var/log 만 쓰기 허용(로그 로테이션 포함). /tmp 는 PrivateTmp. +ProtectSystem=strict +ReadWritePaths=/var/log +# /home 는 읽기전용(자기 코드/config_local.py 읽기만; 쓰기·타 계정 홈 접근 차단). +ProtectHome=read-only +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectControlGroups=true +RestrictSUIDSGID=true +LockPersonality=true +RestrictRealtime=true +# HTTPS(POST)+로컬소켓(DNS/journal)만. 카메라/GPIO 는 /dev char (소켓 아님)라 무관. +# AF_NETLINK 필수 — glibc getaddrinfo/NSS 가 로컬주소 열거에 netlink 를 쓴다 +# (빼면 DNS 해석이 간헐 실패 → 서버 POST 불가). AF_INET/6=HTTPS, AF_UNIX=DNS·journal. +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK +# ※ PrivateDevices 는 쓰지 않는다 — /dev/gpiochip*, 카메라 노드가 숨겨져 동작 불능. + # 재시작 상한 (10분 내 5번 실패 시 중단) StartLimitBurst=5 StartLimitInterval=600s diff --git a/raspberry_pi4/vendor.sh b/raspberry_pi4/vendor.sh index 6cc0b83..8d63627 100644 --- a/raspberry_pi4/vendor.sh +++ b/raspberry_pi4/vendor.sh @@ -38,7 +38,8 @@ CAM="${CAM:-}" if [ -z "${CAM}" ]; then if apt-cache show rpicam-apps >/dev/null 2>&1; then CAM=rpicam-apps; else CAM=libcamera-apps; fi fi -BASE_PKGS="python3-rpi-lgpio python3-requests python3-venv ${CAM}" +# nftables·unattended-upgrades 도 포함 → harden_pi.sh 를 폐쇄망에서 오프라인 실행 가능. +BASE_PKGS="python3-rpi-lgpio python3-requests python3-venv nftables unattended-upgrades ${CAM}" DEPS="$(apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances ${BASE_PKGS} \ | awk '/^\w/{print $1}' | sort -u)" @@ -55,4 +56,23 @@ done [ "${MISS}" = 0 ] || { echo "[ERROR] 필수 .deb 누락 — 반드시 타깃 arch(aarch64 Bookworm)에서 vendor.sh 를 실행하세요."; exit 1; } echo " debs 파일 수: $(ls debs/*.deb 2>/dev/null | wc -l) (필수 패키지 모두 확인됨)" +# 아키텍처 실검사 — 파일명이 아니라 .deb 내부 Architecture 필드로 타깃(arm64/all) 검증. +# (호스트가 amd64면 apt-get download 가 amd64 .deb 를 받아, 타깃 Pi 에서 dpkg 가 +# "wrong architecture" 로 깨진다. 파일명만 보는 검사는 이를 못 잡는다.) +BAD_ARCH=0 +for deb in debs/*.deb; do + [ -e "$deb" ] || continue + arch="$(dpkg-deb --field "$deb" Architecture 2>/dev/null || echo '?')" + case "$arch" in + arm64|all) : ;; + *) echo "[ERROR] arch 불일치: $(basename "$deb") = ${arch} (arm64/all 아님)"; BAD_ARCH=1 ;; + esac +done +if [ "${BAD_ARCH}" != 0 ]; then + echo "[ERROR] 타깃(aarch64/arm64) 이 아닌 .deb 가 있습니다. 호스트 arch=$(dpkg --print-architecture 2>/dev/null || echo unknown)." >&2 + echo " 반드시 arm64 Pi(또는 arm64 chroot/qemu)에서 vendor.sh 를 실행하세요." >&2 + exit 1 +fi +echo " arch 검사 통과 (모든 .deb = arm64/all)" + echo "[vendor] 완료. wheelhouse/ 와 debs/ 를 타깃 Pi 4 로 전송 후 install.sh 실행." diff --git a/raspberry_pi4_sht30/install.sh b/raspberry_pi4_sht30/install.sh index 4e12c4d..5c9abd5 100644 --- a/raspberry_pi4_sht30/install.sh +++ b/raspberry_pi4_sht30/install.sh @@ -52,7 +52,8 @@ if [ "${OFFLINE}" = "1" ]; then exit 1 fi echo " 오프라인 .deb 설치 (apt-get --no-download — 의존성 순서·해결 자동, 네트워크 미사용)" - sudo apt-get install -y --no-download ./debs/*.deb + # --no-install-recommends: vendor.sh 가 Recommends 를 안 담으므로(--no-recommends) 맞춘다. + sudo apt-get install -y --no-install-recommends --no-download ./debs/*.deb else echo " 온라인 apt 설치 (인터넷 필요)" sudo apt-get update @@ -120,6 +121,15 @@ if ! sudo chown "${SERVICE_USER}:${SERVICE_USER}" /var/log/sht30_sensor.log; the fi sudo chmod 640 /var/log/sht30_sensor.log +# config_local.py 즉시 잠금 (API_KEY 포함 — harden 전까지 world-readable 방지) +# 하드닝(harden_pi.sh)이 나중에 다시 조이지만, 그 사이 창(window)에도 노출되지 +# 않도록 설치 시점에 바로 600 으로 잠근다. harden 여부와 무관하게 보호된다. +if [ -f config_local.py ]; then + chmod 600 config_local.py + sudo chown "${SERVICE_USER}:${SERVICE_USER}" config_local.py 2>/dev/null || true + echo " config_local.py 권한 600 (소유자 ${SERVICE_USER}) 설정" +fi + # config_local 안내 (fail closed — 없으면 실행 시 즉시 중단됨) if [ ! -f config_local.py ]; then echo "" diff --git a/raspberry_pi4_sht30/systemd/sht30-sensor.service b/raspberry_pi4_sht30/systemd/sht30-sensor.service index 1d6f653..24fc5db 100644 --- a/raspberry_pi4_sht30/systemd/sht30-sensor.service +++ b/raspberry_pi4_sht30/systemd/sht30-sensor.service @@ -38,6 +38,26 @@ Environment=PYTHONUNBUFFERED=1 # 프로세스 전용 /tmp 격리 PrivateTmp=true +# ── 샌드박싱(하드닝) — 데몬 침해 시 root 승격·파일시스템 오염을 차단 ────────── +# User=pi 는 NOPASSWD sudo 계정이므로 NoNewPrivileges 로 setuid(sudo) 승격 경로를 막는다. +NoNewPrivileges=true +# 루트파일시스템 읽기전용 + /var/log 만 쓰기 허용(로그 로테이션 포함). /tmp 는 PrivateTmp. +ProtectSystem=strict +ReadWritePaths=/var/log +# /home 는 읽기전용(자기 코드/config_local.py 읽기만; 쓰기·타 계정 홈 접근 차단). +ProtectHome=read-only +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectControlGroups=true +RestrictSUIDSGID=true +LockPersonality=true +RestrictRealtime=true +# HTTPS(POST)+로컬소켓(DNS/journal)만. I2C 는 /dev/i2c-1 char (소켓 아님)라 무관. +# AF_NETLINK 필수 — glibc getaddrinfo/NSS 가 로컬주소 열거에 netlink 를 쓴다 +# (빼면 DNS 해석이 간헐 실패 → 서버 POST 불가). AF_INET/6=HTTPS, AF_UNIX=DNS·journal. +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK +# ※ PrivateDevices 는 쓰지 않는다 — /dev/i2c-1 이 숨겨져 센서 접근 불능. + # 재시작 상한 (10분 내 5번 실패 시 중단) StartLimitBurst=5 StartLimitInterval=600s diff --git a/raspberry_pi4_sht30/vendor.sh b/raspberry_pi4_sht30/vendor.sh index daf7d4f..581090d 100644 --- a/raspberry_pi4_sht30/vendor.sh +++ b/raspberry_pi4_sht30/vendor.sh @@ -35,7 +35,8 @@ echo " wheelhouse 파일 수: ${WHEEL_COUNT}" echo "[vendor] (2/2) apt .deb → debs/" mkdir -p debs -BASE_PKGS="python3-requests python3-venv i2c-tools" +# nftables·unattended-upgrades 도 포함 → harden_pi.sh 를 폐쇄망에서 오프라인 실행 가능. +BASE_PKGS="python3-requests python3-venv i2c-tools nftables unattended-upgrades" DEPS="$(apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances ${BASE_PKGS} \ | awk '/^\w/{print $1}' | sort -u)" @@ -52,4 +53,23 @@ done [ "${MISS}" = 0 ] || { echo "[ERROR] 필수 .deb 누락 — 반드시 타깃 arch(aarch64 Bookworm)에서 vendor.sh 를 실행하세요."; exit 1; } echo " debs 파일 수: $(ls debs/*.deb 2>/dev/null | wc -l) (필수 패키지 모두 확인됨)" +# 아키텍처 실검사 — 파일명이 아니라 .deb 내부 Architecture 필드로 타깃(arm64/all) 검증. +# (호스트가 amd64면 apt-get download 가 amd64 .deb 를 받아, 타깃 Pi 에서 dpkg 가 +# "wrong architecture" 로 깨진다. 파일명만 보는 검사는 이를 못 잡는다.) +BAD_ARCH=0 +for deb in debs/*.deb; do + [ -e "$deb" ] || continue + arch="$(dpkg-deb --field "$deb" Architecture 2>/dev/null || echo '?')" + case "$arch" in + arm64|all) : ;; + *) echo "[ERROR] arch 불일치: $(basename "$deb") = ${arch} (arm64/all 아님)"; BAD_ARCH=1 ;; + esac +done +if [ "${BAD_ARCH}" != 0 ]; then + echo "[ERROR] 타깃(aarch64/arm64) 이 아닌 .deb 가 있습니다. 호스트 arch=$(dpkg --print-architecture 2>/dev/null || echo unknown)." >&2 + echo " 반드시 arm64 Pi(또는 arm64 chroot/qemu)에서 vendor.sh 를 실행하세요." >&2 + exit 1 +fi +echo " arch 검사 통과 (모든 .deb = arm64/all)" + echo "[vendor] 완료. wheelhouse/ 와 debs/ 를 타깃 Pi 4 로 전송 후 install.sh 실행." diff --git a/tools/harden_pi.sh b/tools/harden_pi.sh index 813e51e..5cc8528 100644 --- a/tools/harden_pi.sh +++ b/tools/harden_pi.sh @@ -9,28 +9,60 @@ # - 인바운드 전부 차단(nftables), 아웃바운드는 허용(apt/NTP/서버 POST). # - 자동 보안 패치(unattended-upgrades) 켬, 자동 리부팅은 끔(센서 공백 방지). # +# 실행 순서(중요): SSH 차단은 "맨 마지막" 단계다. 그 앞의 실패하기 쉬운 단계 +# (패키지 설치·방화벽·권한)에서 문제가 나면 set -e 로 SSH 가 살아있는 상태에서 +# 중단되어 원격 디버깅이 가능하다. 모든 앞 단계가 성공해야만 SSH 를 끊는다. +# +# 폐쇄망(air-gapped): 같은 폴더에 vendor.sh 산출물 debs/ 가 있으면 nftables· +# unattended-upgrades 도 오프라인(--no-download)으로 설치한다. 없으면 온라인 apt. +# (vendor.sh 가 이 두 패키지도 미리 받아 debs/ 에 포함한다.) +# # 실행: 모듈 설치(install.sh)와 검증(test_connection.py)이 전부 끝난 "마지막"에. # sudo bash harden_pi.sh -# ⚠️ SSH 로 접속해 실행 중이면 완료 즉시 세션이 끊기고 재접속 불가 — 마지막 단계! +# ⚠️ SSH 로 접속해 실행 중이면 마지막 단계에서 세션이 끊기고 재접속 불가. # ============================================================================= set -euo pipefail [ "$(id -u)" -eq 0 ] || { echo "[ERROR] sudo 로 실행하세요: sudo bash harden_pi.sh"; exit 1; } +# debs/ 를 스크립트 위치 기준으로 찾도록 cwd 를 스크립트 폴더로 이동(절대경로 로직엔 무관). +cd "$(dirname "$0")" 2>/dev/null || true -echo "[1/6] SSH 완전 비활성..." -systemctl disable --now ssh.service ssh.socket 2>/dev/null || true -# boot 파티션 `ssh` 파일에 의한 재활성 경로(sshswitch)는 남겨둔다 — 물리 접근 -# 없이는 사용할 수 없고, 현장 복구 수단으로 유용하다. +# 오프라인 번들(debs/) 유무 판정 — install.sh 와 동일한 규칙. +OFFLINE_DEBS=0 +if compgen -G "./debs/*.deb" >/dev/null 2>&1; then + OFFLINE_DEBS=1 + echo "[INFO] 오프라인 번들(debs/) 감지 → 패키지 설치는 네트워크 미사용(--no-download)" +fi -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 +# 필요 패키지 확보: 이미 설치돼 있으면 skip, 아니면 오프라인 debs/ → 온라인 apt 순. +# 설치 후 재검증하여 실패 시 즉시 중단(SSH 아직 살아있음). +ensure_pkgs() { + local missing=() p + for p in "$@"; do dpkg -s "$p" >/dev/null 2>&1 || missing+=("$p"); done + if [ "${#missing[@]}" -eq 0 ]; then + echo " (${*} 이미 설치됨 — skip)" + elif [ "${OFFLINE_DEBS}" = 1 ]; then + echo " 오프라인 debs/ 에서 설치 (네트워크 미사용): ${missing[*]}" + # --no-install-recommends: vendor.sh 가 Recommends 를 번들에 안 담으므로(--no-recommends) + # 설치 때도 Recommends 를 요구하지 않게 맞춘다(폐쇄망 결정성 — 네트워크 폴백 방지). + apt-get install -y --no-install-recommends --no-download ./debs/*.deb + else + echo " 온라인 apt 설치: ${missing[*]}" + # 단독 온라인 실행 대비(로컬 apt 목록이 비어있을 수 있음). install.sh 뒤면 no-op. + apt-get update -q || true + apt-get install -y -q "${missing[@]}" + fi + for p in "$@"; do + dpkg -s "$p" >/dev/null 2>&1 || { + echo "[ERROR] '$p' 설치 실패. 폐쇄망이면 vendor.sh(debs/)에 포함해 재배포하거나," >&2 + echo " 온라인이면 네트워크를 확인하세요. (SSH 는 아직 차단되지 않음 — 복구 가능)" >&2 + exit 1 + } + done +} -echo "[3/6] 방화벽(nftables) — 인바운드 전부 차단..." -apt-get install -y -q nftables >/dev/null +echo "[1/6] 방화벽(nftables) — 인바운드 전부 차단..." +ensure_pkgs nftables cat > /etc/nftables.conf <<'NFT' #!/usr/sbin/nft -f # 센서 노드 방화벽 — outbound 전용 장비. 인바운드는 루프백/응답 트래픽만. @@ -51,8 +83,8 @@ NFT systemctl enable --now nftables nft -f /etc/nftables.conf -echo "[4/6] 자동 보안 패치(unattended-upgrades)..." -apt-get install -y -q unattended-upgrades >/dev/null +echo "[2/6] 자동 보안 패치(unattended-upgrades)..." +ensure_pkgs unattended-upgrades # Raspberry Pi OS 는 기본 Origins 패턴(Debian 전용)에 안 걸린다 — Raspbian/RPF origin 추가. cat > /etc/apt/apt.conf.d/51unattended-upgrades-raspi <<'APT' Unattended-Upgrade::Origins-Pattern { @@ -69,23 +101,42 @@ APT::Periodic::Unattended-Upgrade "1"; APT systemctl enable --now unattended-upgrades 2>/dev/null || true -echo "[5/6] 로컬 권한 조이기..." -# 시크릿 파일 소유자 전용 (API_KEY 포함) +echo "[3/6] 로컬 권한 조이기..." +# 시크릿 파일 소유자 전용 (API_KEY 포함). 못 찾으면 조용히 넘기지 말고 경고. +SECRET_FOUND=0 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"; } + if [ -f "$f" ]; then chmod 600 "$f"; chown pi:pi "$f"; echo " 600: $f"; SECRET_FOUND=1; fi done +[ "${SECRET_FOUND}" = 1 ] || echo "[WARN] config_local.py 를 표준 경로에서 못 찾음 — 배치 경로가 다르면" \ + "수동으로 잠그세요: chmod 600 <경로>/config_local.py (API_KEY 노출 방지)" # 콘솔 자동 로그인 비활성 (물리 콘솔도 로그인 요구) 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 "[4/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 "[5/6] 사전 검증 캡처 (SSH 차단 전 — 방화벽/리스닝 상태)..." echo "-- 리스닝 소켓 (비어 있거나 로컬 전용이어야 함):" ss -lntup || true -echo "-- nftables 룰 (input policy drop 확인):" -nft list chain inet filter input | head -12 +echo "-- nftables input chain (policy drop 확인):" +# || true: pipefail 하에서 head 가 파이프를 먼저 닫아 nft 가 SIGPIPE(141) 로 죽어도 +# 이 검증 단계가 중단되지 않게 한다(SSH 차단 직전 단계라 오탐 중단 방지). +nft list chain inet filter input | head -12 || true + +echo "[6/6] SSH 완전 비활성 (마지막 — 되돌리기 어려운 단계)..." +# 여기까지 왔다면 앞의 모든 단계가 성공한 것. 이제 SSH 를 끊는다. +# boot 파티션 `ssh` 파일에 의한 재활성 경로(sshswitch)는 남겨둔다 — 물리 접근 +# 없이는 사용할 수 없고, 현장 복구 수단으로 유용하다. +systemctl disable --now ssh.service ssh.socket 2>/dev/null || true +echo "-- ssh 상태 (enabled 가 아니어야 함):" +systemctl is-enabled ssh.service 2>&1 || true + echo "" -echo "완료. 위 출력을 캡처해 보안 증적으로 보관할 것." +echo "완료. 위 [5/6]·[6/6] 출력을 캡처해 보안 증적으로 보관할 것." echo "리부팅 권장: sudo reboot (이후 ss -lntup 재확인)"