3P by GN⁺ 4일전 | ★ favorite | 댓글 1개
  • ssh-init-vm은 새 VM의 첫 SSH 접속에서 중간자 공격을 막기 위해 cloud-init으로 임시 SSH 호스트 개인키를 주입하고, 장기 호스트 키를 생성·가져오는 동안만 신뢰하게 함
  • Hetzner Cloud처럼 전용 접속 보호 기능이 없는 VPS나 클라우드에서도 동작하며, 필요한 것은 널리 지원되는 cloud-init뿐임
  • 일반적인 Trust On First Use에서 ssh의 “The authenticity of host [...] can't be established” 질문에 yes를 입력하면, 공격자가 트래픽을 프록시하거나 사용자의 VM처럼 보이는 머신을 제공할 수 있음
  • 장기 SSH 호스트 개인키를 cloud-init userdata에 직접 넣으면 첫 접속 인증에는 도움이 되지만, 메타데이터 서비스·SSRF·클라우드 제공자 시스템·관리자 워크스테이션을 통해 민감한 키 자료가 노출될 수 있음
  • ssh-init-vm은 임시 키를 임시 디렉터리에 두고 ~/.ssh/known_hosts에 넣지 않으며, VM 출력물을 그대로 저장하지 않고 OpenSSH의 호스트 키 순환에 의존해 장기 키를 기록함

cloud-init userdata 노출 문제

  • cloud-init으로 장기 SSH 호스트 개인키를 주입하면 공개키를 ~/.ssh/known_hosts에 넣어 첫 접속을 인증할 수 있지만, 개인키가 여러 경로로 유출될 수 있음
  • VM 내부의 임의 프로세스가 일반적으로 읽을 수 있는 메타데이터 서비스에서 userdata를 얻을 수 있으며, Hetzner VM에서는 http://169.254.169.254/hetzner/v1/userdata로 cloud-init 내용이 보일 수 있음
  • 공격자는 SSRF를 통해 프로세스가 메타데이터를 누설하도록 만들 수 있고, 이런 차단은 전용 보호 기능이 있는 환경에서도 항상 적용되는 것은 아님
  • 클라우드 제공자의 다른 시스템에서도 userdata가 노출될 수 있으며, Hetzner는 서버 생성 API 문서에서 “passwords or other sensitive information”을 저장하지 말라고 명시
  • 관리자 워크스테이션도 cloud-init userdata가 남거나 지나가는 위치가 될 수 있으므로, 장기 개인키를 넣는 방식은 키가 유효한 동안 노출될 위험을 만듦

보안 분석과 위협 모델

  • 전제는 OpenSSH 프로토콜과 구현을 신뢰하며, 관리자가 공격을 탐지하는 능력에는 의존하지 않는다는 것임
  • 네트워크 공격자에 대한 보호

    • 보호 대상은 관리자 워크스테이션의 무결성과 VM임
    • 공격자는 네트워크를 완전히 제어하는 중간자이며, 스크립트가 성공하거나 실패해 종료된 뒤 어느 시점에 cloud-init userdata를 알게 될 수 있음
    • 공격자가 키 자료를 가치가 남아 있는 시점에 알지 못하기 때문에 보호가 성립함
    • 스크립트는 임시 SSH 호스트 키의 우발적 사용을 막기 위해 이를 임시 디렉터리에 보관하며, 임시 SSH 호스트 키를 ~/.ssh/known_hosts에 넣지 않음
  • 관리자 워크스테이션이 침해된 경우

    • 보호 대상은 VM과 VM의 장기 SSH 호스트 개인키에 한정됨
    • 공격자는 네트워크와 관리자 워크스테이션을 완전히 제어하지만, 실제 VM에는 접속하지 않는다고 가정함
    • 장기 SSH 호스트 개인키는 관리자 워크스테이션에 있었던 적이 없고, 공격자가 실제 VM에 접속하지 않기 때문에 VM의 장기 호스트 키를 얻지 못함
    • 공격자가 실제 VM에 접속하면 ssh root@<VM> cat /etc/ssh/ssh_host_* 같은 방식으로 SSH 호스트 키를 알 수 있을 가능성이 큼
  • VM 또는 제공자가 침해된 경우

    • 보호 대상은 관리자 워크스테이션의 무결성에 한정됨
    • 공격자는 네트워크를 완전히 제어하고 VM이나 제공자도 완전히 제어할 수 있음
    • 이 경우에도 OpenSSH가 안전하다는 전제 때문에 관리자 워크스테이션의 무결성을 보호함
    • 추가 방어로 스크립트는 VM 출력물을 그대로 ~/.ssh/known_hosts에 쓰지 않고, OpenSSH 순환에 의존해 장기 SSH 호스트 키를 넣음
    • 이 방식은 침해된 호스트가 known_hosts 파서에 악성 데이터를 먹이는 것을 막고, VM이 실제로 제어하는 키~/.ssh/known_hosts에 기록되게 함
    • HashKnownHosts 같은 OpenSSH 옵션과 향후 관련 옵션도 올바르게 처리할 수 있음

실제 중간자 공격이 성립하는 조건

  • 중간자 공격의 성공 여부는 사용자가 모든 접속이 처음부터 잘못된 머신으로 향하고 있음을 실제로 감지하는지, 비밀번호 입력을 거부하는지, ssh의 agent 또는 X11 전달을 설정하는지에 따라 달라짐
  • ssh-mitm 기준의 단순화된 조건으로는, 공격자가 진짜 대상 호스트가 아니라 공격자 제어 머신에 접근 권한을 제공해 사용자를 속일 수 있으면 성공 가능성이 큼
  • 공격자가 사용자를 속여 진짜 호스트에 로그인할 수 있는 정보를 얻어도 성공함
    • 사용자가 공격자 머신에 비밀번호로 로그인하면 공격자가 성공할 수 있음
    • 사용자가 어떤 인증 방식으로든 로그인한 뒤 프롬프트에서 비밀번호를 입력하면 공격자가 성공할 수 있음
    • 사용자가 어떤 인증 방식으로든 로그인하고 ssh-agent 연결을 전달하면 공격자가 성공할 수 있음
  • 위 조건이 없으면 공격자는 사용자를 속이기 위해 진짜 호스트 접근이 필요하지만, 사용자의 입력만으로는 진짜 호스트에 로그인할 수 없어 실패할 가능성이 큼
  • 사용자가 X11 연결을 전달하면 공격자는 인증 방식과 별개로 관리자 워크스테이션을 공격하는 데 성공할 수도 있음
Lobste.rs 의견들
  • 자동화할 수 있어서 멋짐. 수동 방식으로는 클라우드 제공자 콘솔에서 서버의 SSH 지문을 별도 채널로 확인해 왔음
    관리하는 클라우드 인스턴스가 많지 않아서, 프로비저닝에 수동 단계가 몇 개 있어도 괜찮음

    • 그 방식도 동작함. 다만 사용하던 제공자가 그 부분을 연결해 두지 않았고, 동시에 설정 자동화 스크립트를 만들고 있던 상황이었음
  • DNS 영역을 자동화해 뒀다면 다른 접근도 가능함: 범위가 매우 좁은 일회용 토큰을 만들고, 예를 들어 my-server-hostname.example.net에 레코드 하나 생성만 허용하게 함
    그 토큰을 cloud-init으로 서버에 전달하고, 서버가 공개 SSH 키를 DNS의 SSHFP 레코드로 올리게 함. 이후 SSH 클라이언트가 SSHFP 레코드를 자동 검증하게 할 수 있으며, DNS 영역은 DNSSEC 서명이 되어 있어야 함
    이 흐름은 서버가 비공개 SSH 호스트 키를 유지하면서도 키 회전을 피할 수 있게 해줌. 대부분의 DNS 제공자는 이런 세밀한 일회용 접근 토큰을 지원하지 않지만, 토큰을 검증한 뒤 영구적이고 범위 제한 없는 토큰으로 대신 API 호출을 해주는 간단한 내부 웹 서비스를 둘 수 있음. SSH 서버는 그 영구 토큰에 접근하지 못함

    • 창의적인 방식이고, 커스텀 서비스용 일회용 토큰이면 꽤 유연해서 잘 동작할 듯함
      다만 DNSSEC 도메인에 쓰기보다는 SSH 인증서를 생성하는 쪽을 더 선호할 것 같고, 그 지점부터는 어떤 해법이 특정 환경에 더 잘 맞는지의 문제가 됨
      이렇게 유연한 토큰을 생성할 수 있는 소프트웨어나 제공자를 알고 있는지, 아니면 어느 정도 직접 개발이 필요한지 궁금함
  • 꽤 깔끔함. 그런데 글의 날짜에서 월과 일이 뒤바뀐 것 같음

    • OpenSSH로 작업하는 건 늘 즐겁고, 월/일 표기는 고쳐 둠
  • 예전에 같은 SSH 닭과 달걀 문제를 탐색하려고 실험적인 서비스를 만든 적이 있음
    요청 시 SSHFP DNS 레코드를 생성해 주며, 관심 있으면 https://github.com/tedb/sshfp 를 보면 됨

  • VM 제공자들 사이에서 어느 정도 일관적인 169.254.169.254 메타데이터 서비스를 다뤄줘서 반가움. cloud-init 소스의 여러 cloudinit/source/DataSource*.py 항목을 보면 확인할 수 있음
    개인적으로는 cloud-init의 설계와 한계 때문에 점점 피로감을 느낌. 로컬 QEMU 가상머신, 원격 머신, 컨테이너, 물리 하드웨어 전반에서 시스템 설정을 통합하는 데 관심이 있음
    arch-boxes project는 ArchLinux cloud-init image가 어떻게 만들어지는지 보여주며, 매우 단순한 셸 스크립트 묶음임. 이 방법을 guestfishµvm으로 응용하면, OCI 호환 이미지, QEMU나 클라우드 제공자용 디스크 이미지, 새 물리 머신 프로비저닝에 완전히 같은 스크립트를 쓸 수 있음
    몇 가지 QEMU 플래그와 함께라면 cloud-init 의존 없이 같은 접근을 재현할 수 있음. 아는 한 systemd.system-credentials로 임시 호스트 키를 전달할 수는 없고, ssh.authorized_keys.root 같은 ~/.ssh/authorized_keys용 자격 증명만 있음
    대신 initrd 단계에서 실행되거나 systemd-firstboot.service와 함께 실행되는 unit 파일을 만들 수 있음. 이 unit 파일은 이미지에 미리 넣거나 systemd.extra-unit.* 자격 증명으로 임시 주입하고, systemd.wants=… 커널 명령줄 옵션으로 활성화할 수 있음. QEMU에서는 -netdev user,id=metadata,net=169.254.0.0/16,dhcpstart=169.254.0.15,guestfwd=tcp:169.254.169.254:80-cmd:… 항목으로 메타데이터 서비스 존재를 흉내낼 수 있음. 다만 생성된 인터페이스를 활성화해야 할 가능성이 크고, 이 역시 임시 unit 파일로 처리하는 편이 나을 수 있음
    이렇게 하면 여러 종류의 “머신”에서 일관된 시스템 설정을 수행할 때, 비교적 낮은 복잡도로 상당한 유연성을 얻음. 실제로 이 작업만 놓고 보면, 이미지 생성 도구가 고정 호스트 키가 들어간 머신 이미지를 만들고, 첫 재부팅이나 종료 때 실행되는 커스텀 호스트 키 회전 스크립트를 SystemD 서비스로 설치하는 방식이 가장 좋아 보임

    • ArchLinux에서는 /etc/mkinitcpio.conf에서 systemd HOOK을 활성화하면, initrd에서 수행할 작업을 위해 SystemD unit 파일을 작성할 수 있고 사실상 그래야 함
      실제로 써보면 {/etc,/usr/lib}/initcpio/hooks를 작성하는 것보다 아주 약간 더 번거로운 정도임
      하지만 initrd에서 systemd-networkingsystemd-resolved를 켜는 건 꽤 쉽기 때문에, initrd가 시스템 시작 책임을 맡고 루트 파일시스템으로 전환하기 전에 작업을 예약할 수 있음
      물론 노트북 같은 물리 하드웨어에서는 Wi-Fi 연결에 NetworkManager 같은 것이 필요해 잘 안 맞을 수 있지만, QEMU VM과 호스팅 VM에는 잘 맞고 많은 시스템 시작 작업이 이 공간에 자연스럽게 들어감
      목표는 cloud-init에 의존하지 않고, 특정 클라우드 제공자 하나에 묶이지 않으며, 물리 머신·컨테이너·로컬 VM·호스팅 VM 전반의 일관성을 얻고, 의존성을 사실상 SystemD 정도로 줄이는 것임
    • 원하는 기능만 확장하면 되는 아주 작은 도구로는 https://github.com/the-maldridge/shinit/ 같은 것도 가능하지 않을까 싶음