TL;DR

  • tpmlsm은 바이너리 경로와 SHA-256 해시로 허용 목록을 구성해, root 권한을 포함해 목록에 없는 프로그램의 /dev/tpm0 및 /dev/tpmrm0 접근을 차단하는 eBPF 기반 커널 가드임
  • BPF LSM과 bpf_ima_file_hash를 사용하며, 파일이 바뀌거나 다른 경로로 복사되면 허용 목록에서 제외됨
  • 허용된 tpm2_getrandom은 TPM에서 난수를 읽고 cat은 장치 열기가 거부되는 동작을 확인할 수 있음
  • Linux 5.18 이상과 필수 커널 설정이 필요하며, Ubuntu에서는 BPF LSM 활성화와 SHA-256 IMA 해시 설정이 요구됨
  • 허용 바이너리의 동작 범위, 로드 라이브러리, 실행 중인 프로세스, root의 핀 삭제 등 제한이 있으며, 저장소 코드는 고객 환경에서 실행되는 코드가 아닌 참고 구현임

실행

  • tpmlsm은 Linux 시스템에서 TPM을 열 수 있는 프로그램을 지정하며, 바이너리 목록을 컴파일하면 목록에 없는 프로그램의 /dev/tpm0 또는 /dev/tpmrm0 열기를 커널이 거부함
  • 이 제한은 root 권한으로 실행되는 프로그램에도 적용되며, 그 밖의 시스템 동작은 이전과 동일함
  • 허용 항목은 바이너리 경로와 파일 내용의 SHA-256 해시로 식별됨. 파일을 변경하거나 다른 위치에 복사하면 허용 대상에서 제외됨
  • 목록에 tpm2_getrandom을 넣으면 root로 실행해도 TPM에서 난수를 읽을 수 있지만, cat의 /dev/tpm0 접근은 Operation not permitted 오류로 거부됨
  • -watch 옵션을 사용하면 tpmlsm이 포그라운드에서 실행되며 허용·거부된 TPM 열기 시도를 기록함. 예시에서는 tpm2_getrandom이 ALLOW, cat이 DENY로 표시됨
  • 고객용 상호 TLS(mTLS) 개인 키를 TPM으로 보호하는 eBPF 및 가상 TPM(vTPM) 작업 중 시제품을 제작함. 저장소는 블로그 게시물 ‘A TPM bouncer in eBPF’와 함께 공개한 참고 구현이며, 고객 환경에서 실제 실행되는 코드는 아님

요구 사항

  • bpf_ima_file_hash를 위해 Linux 커널 5.18 이상이 필요하며, Ubuntu 24.04의 6.8.0-146-generic에서 테스트됨
  • 필요한 커널 설정은 CONFIG_BPF_SYSCALL=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_BPF_LSM=y, CONFIG_IMA=y임. CONFIG_BPF_LSM은 부팅 시에도 활성화해야 하며, IMA는 실행 시 바이너리를 해시함
  • /boot/config-$(uname -r)에서 grep $OPTION으로 각 설정의 활성화 여부를 확인할 수 있음
  • BPF LSM은 활성 LSM 목록에도 포함돼야 하며, Ubuntu에서는 기본적으로 포함되지 않음. /sys/kernel/security/lsm을 확인하고 bpf가 없으면 목록에 추가한 뒤 커널 명령줄에 lsm=으로 전달해야 함
  • Ubuntu 설정 예시는 /etc/default/grub.d/99-tpmlsm.cfg에 lsm=lockdown,capability,landlock,yama,apparmor,bpf ima_hash=sha256을 추가하고, update-grub을 실행한 다음 재부팅하는 방식임
  • BPF LSM이 활성화되지 않으면 tpmlsm은 시작을 거부함. IMA 해시 알고리즘은 SHA-256이어야 하며, Ubuntu의 기본값인 ima_hash=sha256 외의 알고리즘을 사용하면 모든 바이너리가 거부됨

허용 목록

  • TPM을 열 수 있어야 하는 각 바이너리를 allowlist.txt에 sha256sum 형식으로 추가해야 함. 형식은 SHA-256 해시 뒤에 파일의 실제 경로를 두는 방식임
  • /usr/bin/tpm2_getrandom의 실제 경로를 확인해 해시와 함께 목록에 추가한 뒤 make로 다시 빌드하는 절차임
  • 허용 목록이 비어 있거나 형식이 잘못되면 tpmlsm은 시작을 거부함
  • TPM을 사용하는 프로그램보다 먼저 부팅 때마다 sudo ./tpmlsm을 실행해야 함. 허용·거부된 열기 시도를 기록하려면 -watch를 추가해 포그라운드에서 실행함
  • 목록 항목을 제거하는 명령은 없음. 목록 변경에는 새 tpmlsm을 빌드해 배포하고 재부팅해야 함
  • root는 핀을 삭제할 수 있으며, 관련 제한 사항은 아래와 같음

제한 사항

  • 바이너리 하나를 허용하면 그 바이너리가 수행할 수 있는 모든 동작이 허용됨. python3을 허용하면 모든 Python 스크립트가 허용되며, Ubuntu에서는 모든 tpm2_* 명령이 하나의 tpm2 바이너리임
  • 검사 대상은 바이너리뿐이며 로드하는 라이브러리는 검사하지 않음. 허용 프로그램을 실행할 수 있는 사용자는 LD_PRELOAD로 코드를 주입할 수 있고, root는 /etc/ld.so.preload나 디버거도 사용할 수 있음. CGO_ENABLED=0으로 빌드한 Go 프로그램처럼 정적 바이너리를 선호하는 방식임
  • ext4에서 테스트됨. btrfs에서는 파일 조회가 일치하지 않아 모든 TPM 열기가 거부됨
  • 허용 바이너리를 업데이트하면 새 tpmlsm을 배포하고 재부팅할 때까지 해당 바이너리의 접근이 차단됨
  • tpmlsm 로드 당시 이미 실행 중인 프로그램은 재시작할 때까지 접근이 거부됨
  • root는 핀 삭제, 커널 모듈 로드, 다른 커널로 부팅해 tpmlsm을 끌 수 있음. 보안 부팅(Secure Boot)과 커널 잠금(kernel lockdown)의 두 모드는 이를 더 어렵게 함
  • 핀 삭제, bpffs 언마운트, 프로그램 분리를 거부하는 후크를 추가하면 시스템이 실행되는 동안 나머지 우회 경로도 막을 수 있으며, 재부팅만 가능한 상태가 됨. 이후 원격 검증자는 재부팅 뒤 tpmlsm이 다시 로드됐는지 증명(attestation)으로 확인할 수 있음

개발

의존성

  • Go 1.26 이상
  • LLVM/clang
  • libbpf 헤더(libbpf-dev)
  • bpftool(linux-tools)
  • make
  • Ubuntu에서는 clang, llvm, libbpf-dev, golang-go, linux-tools-$(uname -r), linux-tools-common, make 패키지를 설치하는 방식임

빌드

  • make로 빌드함
  • cmd/tpmlsm의 생성된 BPF 객체가 저장소에 포함돼 있어 일반 빌드에는 Go만 필요함
  • 빌드는 현재 사용 중인 시스템용으로 생성됨. macOS에서는 make GOOS=linux로 Linux 바이너리를 빌드할 수 있음
  • bpf/tpmlsm.c를 변경한 뒤 객체를 다시 생성하려면 Linux에서 make generate를 실행함

라이선스

  • Go 코드는 BSD 3-Clause License로 제공됨
  • bpf/의 BPF 코드는 GPL-2.0-only와 BSD 3-Clause License의 이중 라이선스이며, 둘 중 하나의 조건을 선택해 사용할 수 있음

로고 크레딧

  • 경비원 고퍼 그림은 Renee French가 디자인한 Go 고퍼를 바탕으로 함