집에서 개인 서버를 운영하며 7개월 동안 인프라를 다시 구축한 경험을 소개한다. 단일 노트북에서 Incus OS와 컨테이너를 사용해 서비스를 자동 배포·업데이트하고, 기본적으로 네트워크를 차단하는 구조다. 보안과 자동화를 중시하는 사람에게 참고가 될 수 있지만, 가정용 Nextcloud 서버에는 필요 이상으로 복잡할 수 있으며 고가용성도 제공하지 않는다.
구성과 운영 방식
서버는 인터넷 공유기 옆에 둔 일반 노트북으로, 메모리 16GB와 NVMe SSD, 네트워크 카드가 있다. 운영체제는 시스템 컨테이너와 가상 머신을 관리하는 Incus만 실행하는 불변 시스템 Incus OS다. 그 위에 서비스별 Ubuntu 시스템 컨테이너 약 20개와 민감한 작업을 위한 가상 머신 2개를 둔다. 각 시스템 컨테이너 안에서는 애플리케이션을 Podman 컨테이너로 실행하며, systemd가 서비스로 바꾸는 quadlet 파일을 사용한다. 따라서 Incus 인스턴스와 그 안의 Podman 컨테이너, 두 단계의 컨테이너가 있다.
운영 중인 서비스는 Nextcloud, CI 러너가 포함된 Forgejo, 스마트폰 사진·동영상을 저장하는 Immich, PeerTube, Vaultwarden, Kanboard, 네트워크 감시용 Uptime Kuma와 SmokePing, 정적 웹사이트, Prometheus·Grafana·Loki·Alertmanager 모니터링 구성, CrowdSec, restic 백업이다.
설정은 수동으로 하지 않는다. OpenTofu(Terraform의 오픈소스 포크)와 첫 부팅 때 시스템을 설정하는 cloud-init을 사용해 Git 저장소에 선언한다. 시스템 컨테이너는 비특권 방식으로 실행하므로 컨테이너 안의 root 사용자는 호스트의 root가 아니다. 서비스마다 가상 머신을 두는 것보다 메모리와 디스크를 적게 쓰고 빠르게 동작한다는 것이 운영 경험에서 느낀 장점이다.
Incus OS는 가능한 한 잠긴 시스템을 목표로 선택했다. Secure Boot가 켜져 있고, A/B 부팅을 지원해 업데이트를 다른 파티션에 기록하며 필요하면 이전 시스템으로 돌아갈 수 있다. 로컬 셸, SSH, 패키지 관리자 등이 없어 노트북에 직접 접근해도 로컬에서 할 수 있는 작업이 없다. TPM을 이용한 디스크 암호화 덕분에 암호를 입력하지 않고도 자동 재부팅할 수 있다. 다만 노트북을 도난당하면 부팅이 가능하므로, 공격자가 내 LAN에 있는 것처럼 시스템을 공격할 수 있다.
Incus OS는 인터넷에 공개하지 않는다. LAN에서 인증서로 인증하는 Incus API를 통해 관리한다. 이 구성의 부수 효과로 인프라 관리에 SSH를 더는 쓰지 않는다. SSH 키를 관리하거나 SSH 접점을 보호할 필요가 없고, TERM 변수 문제도 없다. 인스턴스 안을 확인해야 할 때는 Incus API를 거쳐 incus exec으로 명령을 실행하거나 incus shell로 셸을 연다. 전체 구성에서 SSH를 쓰는 곳은 애플리케이션 기능인 Forgejo의 Git over SSH뿐이다.
인스턴스 재생성과 네트워크 차단
각 인스턴스는 새 Ubuntu 클라우드 이미지에서 생성한다. 첫 부팅 때 cloud-init이 시스템을 업그레이드하고 Podman을 설치하며, quadlet 파일을 작성하고 비밀 정보를 복호화한 뒤 서비스를 시작한다. Qubes OS 사용 경험에서 아이디어를 얻었다.
인스턴스의 설정 파일, 컨테이너, 명령 또는 명령 순서를 바꾸면 OpenTofu가 해당 인스턴스를 삭제하고 처음부터 다시 만든다. 구성 드리프트가 없고 실행 중인 상태가 Git에 선언한 내용과 일치한다. 누군가 컨테이너에 침입해 흔적을 남겨도 다음 재생성 때 사라진다. 지속되는 데이터는 Incus 볼륨에 따로 둔다.
- 애플리케이션의 명명된 볼륨이 저장되는 Podman 저장소 볼륨
- 인스턴스의 비밀 정보 복호화 키를 보관하는 볼륨
- 재생성 속도를 높이기 위한 apt 패키지 로컬 캐시
시스템 부분은 전부 일회성이므로 애플리케이션이 Podman 컨테이너에서 실행되는 한 Ubuntu를 다른 배포판으로 바꾸는 일도 어렵지 않을 수 있다. 대신 변경할 때마다 인스턴스를 다시 만드는 동안 해당 구성 요소를 1~2분 동안 사용할 수 없다. 그 대가로 변경 사항이 다음 부팅에도 작동할지 확인할 수 있고, 필요하면 시스템 전체를 다시 만들 수 있다.
모든 인스턴스는 내부 브리지에 연결되며 Incus ACL(접근 제어 목록)은 양방향 통신을 기본적으로 모두 거부한다. 인스턴스끼리의 통신도 차단한다. 허용할 연결은 이유를 설명해 OpenTofu 코드에 선언한다. 예를 들어 리버스 프록시는 각 백엔드의 지정 포트에만 접근하고, Prometheus는 메트릭 수집기에 접근하며, 모든 인스턴스는 로그를 Loki로 보낸다. 반면 Nextcloud는 Vaultwarden과 통신할 수 없다. 서비스 하나가 침해되더라도 공격자가 다른 서비스로 쉽게 이동하지 못하게 하려는 구성이다.
민감도가 가장 높은 부분에는 가상 머신 두 대를 쓴다. haproxy 가상 머신만 물리 네트워크에 연결돼 인터넷 트래픽을 받는 DMZ 역할을 한다. CI 작업은 임의의 코드를 실행하므로 CI 러너도 가상 머신으로 분리했다. 이 러너는 권한이 필요한 중첩 컨테이너 실행을 위해 Docker in Docker도 사용한다. 컨테이너 격리는 가상 머신보다 약하지만, 현재 위협 모델에는 괜찮다고 판단했다. 신뢰할 수 없는 입력에 더 많이 노출되는 부분에는 하이퍼바이저 수준의 보안을 택했다.
나가는 연결과 외부 공개 서비스
대부분의 인스턴스는 인터넷에 직접 접속할 수 없다. 전용 인스턴스 하나가 유일한 출구 역할을 하며 다음 기능을 제공한다.
- 허용 도메인 목록에 있는 곳만 접속할 수 있는 HTTP 프록시. 소비자들이 같은 도메인 목록을 공유하며, 이 구성에서는 괜찮다고 판단했다.
- apt 패키지용 캐시 프록시인 apt-cacher-ng.
- 사용하는 컨테이너 레지스트리인 Docker Hub, GitHub, Codeberg의 로컬 미러(풀스루 캐시).
모든 인스턴스는 패키지와 컨테이너 이미지를 이 경로로 받는다. 인터넷 접속이 필요한 소수의 애플리케이션도 필터링 프록시를 거친다. 공개 리버스 프록시의 ACME 인증서 발급, CI 러너, Uptime Kuma 점검, S3 백업처럼 프록시를 사용할 수 없는 예외는 ACL 규칙에 명시하고 이유를 기록한다.
서비스가 해킹되더라도 임의의 서버로 데이터를 빼내거나 추가 공격 코드를 내려받기 어렵게 하는 것이 목적이다. 인스턴스 20개를 다시 만들어도 상위 미러에 부하를 주거나 네트워크 대역폭을 낭비하지 않는다.
외부에서 들어오는 트래픽은 다음 경로를 거친다.
Internet
|
v
haproxy VM (CrowdSec bouncer with nftables)
| raw TCP + PROXY protocol
v
caddy (public) --> public services
haproxy 가상 머신은 원시 TCP만 전달한다. 암호화되지 않은 트래픽을 보거나 인증서·개인 키를 보유하지 않는다. 연결 시작 부분에 원래 클라이언트 IP를 담는 짧은 헤더를 추가하는 PROXY 프로토콜을 사용한다. NAT와 컨테이너를 거치더라도 caddy가 실제 접속자의 IP를 알 수 있다.
같은 가상 머신의 CrowdSec 방화벽 바운서는 평판이 나쁜 IP의 연결을 nftables에서 차단한다. haproxy가 연결을 받기도 전이다. CrowdSec은 Loki에서 caddy, Forgejo, Nextcloud 로그를 읽어 비정상 행동을 탐지하고 커뮤니티 차단 목록도 사용한다. 이 방식은 경로 전체에서 실제 클라이언트 IP를 알고 있어야 작동한다. 이후 caddy가 TLS를 종료하고 요청을 서비스로 전달한다.
공개 웹사이트용 caddy와 VPN에서만 접근할 수 있는 사이트용 caddy는 별도로 운영한다. 공개 caddy가 해킹되더라도 VPN 전용 서비스에는 영향을 주지 않도록 하기 위해서다. VPN용 caddy는 호스트의 Tailscale 주소에서만 요청을 받고, 클라이언트 IP가 VPN 대역에 속하는지도 확인한다. Alertmanager, Uptime Kuma, Kanboard 같은 관리 도구는 공개 접점을 두지 않는다.
일부 도구는 인터넷에 공개하지만 관리 인터페이스나 관리 API는 VPN에서만 접근할 수 있다. 관리 API에 취약점이 있어도 이를 악용하기 어렵게 하려는 조치다. 예를 들어 Nextcloud는 공개와 VPN 양쪽에서 쓸 수 있지만 관리 API는 VPN에서만 접근 가능하다.
사진과 동영상을 관리하는 Immich는 공개 접점을 더 제한했다. 인터넷에서는 공유 링크에만 응답하므로 외부 사람과 콘텐츠를 공유할 수 있다. 라이브러리 탐색과 사진·동영상 업로드 등 나머지 기능은 VPN을 거쳐야 한다. 전체 애플리케이션을 인터넷에 공개하지 않는 보안 조치다.
VPN 전용 사이트에도 자체 서명 인증서가 아닌 실제 TLS 인증서를 쓰기 위해 자체 인증 기관(CA)을 운영한다. Let’s Encrypt와 같은 프로토콜인 ACME를 통해 인증서를 발급한다. 루트 인증서를 모든 인스턴스와 휴대전화·컴퓨터·태블릿에 설치해 해당 루트 CA를 신뢰하는 구성 요소가 인증서를 인식하게 했다. 따라서 curl -k를 쓸 필요가 없다.
비밀 정보와 자동 업데이트
비밀 정보는 평문으로 저장소에 커밋하지 않는다. sops와 age로 암호화해 서비스 파일과 함께 Git에 보관한다. sops GitHub 페이지
각 인스턴스는 한 번 생성한 age 개인 키를 가지며, 키는 secrets 볼륨에 저장되고 서버 밖으로 나가지 않는다. 서비스별 비밀 정보는 해당 인스턴스의 키와 내 워크스테이션 키만으로 복호화할 수 있게 암호화한다. 따라서 비밀 정보를 알아야 하는 사람이나 기기만 읽을 수 있다. 저장소 전체를 복사해도 침해된 Nextcloud가 Vaultwarden의 비밀 정보를 읽을 수 없다.
부팅 때 비밀 정보는 일회용 루트 디스크에서 복호화하므로 인스턴스를 재생성할 때마다 다시 만들어진다. 혹시 모를 경우를 대비해 gitleaks를 실행하는 Git 사전 커밋 훅도 둔다. 기록에 비밀 정보가 발견되면 커밋을 막는다.
전체 시스템은 자동으로 업데이트된다.
- Incus OS는 불변 이미지로 자체 업데이트한다.
- Ubuntu 인스턴스는 unattended-upgrades를 사용한다. 인스턴스를 만들 때도 전체 업그레이드를 수행해 최신 패키지로 시작하게 한다. 다만 현재는 업그레이드를 제자리에서 하는 대신 기본 이미지를 통째로 갱신하는 방법을 아직 정하지 못했다.
- Podman 컨테이너는 서비스로 실행되는
podman auto-update가 업데이트한다. 설정된 태그의 새 이미지를 내려받으며, 새 컨테이너가 시작에 실패하면 이전 상태로 되돌린다.
postgres:18-alpine처럼 주요 버전 태그를 선택해 주요 버전 업그레이드는 Git에서 한 줄을 의도적으로 바꾸도록 했다. 지금까지 컨테이너 자동 업데이트 후 문제를 겪은 적은 없다. 사용 중인 각 프로젝트의 릴리스 RSS 피드를 구독해 호환성을 깨는 변경이나 새 주요 버전이 나왔는지 빠르게 확인한다.
백업과 모니터링
백업은 두 겹으로 구성했다.
- Incus가 각 볼륨의 ZFS 스냅샷을 자동으로 만든다. 데이터를 대상으로 매일 스냅샷을 생성하고 2주간 보관해 빠르게 복구할 수 있다.
- restic이 데이터 볼륨을 매일 S3 저장소에 백업하며, 서버 밖으로 보내기 전에 클라이언트에서 암호화한다.
공격자가 restic 인스턴스를 장악하면 버킷에 접근해 백업을 삭제할 수 있다. 이를 막기 위해 버킷에 버전 관리와 보호 기능을 설정했다. 데이터를 삭제해도 정해진 기간 동안 복원할 수 있다. 다만 restic은 실행할 때마다 파일을 수정하므로 버킷을 불변 모드로 설정하지 않았다. S3를 이용한 보안 백업 설명
전체 시스템을 모니터링한다. Prometheus는 모든 인스턴스와 리버스 프록시, haproxy, CrowdSec, Incus 자체에서 메트릭을 수집한다. 각 인스턴스의 Vector 사이드카는 systemd 저널을 Loki로 보내 모든 서비스 로그를 한곳에 모은다. Alertmanager는 서비스가 실패하거나 실행되지 않으면 알림을 보낸다. Grafana에서는 데이터를 살펴보고 caddy, haproxy, CrowdSec, Incus 활동을 보여주는 대시보드도 확인한다. Loki는 CrowdSec의 데이터 소스이기도 하므로 로그를 디버깅과 보안 양쪽에 활용한다.
보안상의 이점과 한계
보안에서 자주 쓰는 세 기준은 기밀성(권한이 있는 사람만 데이터를 읽을 수 있음), 무결성(허가 없이 데이터와 시스템이 변경되지 않음), 가용성(필요할 때 서비스를 쓸 수 있음)이다. 이런 기준은 인증 환경에서 주로 접하지만, 사이버 보안의 여러 측면을 정리하는 데 유용하다고 본다.
기밀성에는 인스턴스별로 암호화한 비밀 정보, 데이터 유출을 어렵게 하는 외부 연결 필터링, 침해된 서비스가 다른 서비스에 접근하지 못하게 하는 네트워크 ACL이 기여한다. 관리 인터페이스는 VPN에서만 접근할 수 있다. 인터넷 경계의 가상 머신은 TLS 키를 보유하지 않고 내부에서만 TLS를 종료한다. restic은 백업을 서버 밖으로 보내기 전에 암호화한다.
무결성에는 Secure Boot와 불변 시스템인 Incus OS, 로컬 캐시를 통해 알려진 출처에서만 가져오는 패키지와 이미지가 관련된다. 수동 변경을 하지 않고 Git을 유일한 기준으로 삼으며, 인스턴스를 Git에서 재생성한다. 따라서 침입자가 루트 디스크에 남긴 흔적은 어느 정도 지속되지 않는다.
가용성 측면에서는 CrowdSec이 알려진 악성 IP, 이상한 요청, 무차별 대입 시도를 차단한다. 자동 업데이트는 취약점이 열린 채로 남는 시간을 줄인다. ZFS 스냅샷으로 빠르게 되돌리고 restic으로 전체를 복원할 수 있다. S3 버전 관리는 백업 삭제에 대비하며, 모니터링과 알림은 문제를 빠르게 알려준다. 저장소에서 인프라 전체를 다시 구축할 수도 있다.
가용성의 가장 큰 한계는 모든 것이 집의 단일 장비에서 실행된다는 점이다. 장비가 고장 나거나 인터넷 연결이 끊기면 전체 서비스가 중단된다. 공개 caddy나 haproxy를 재구성할 때도 외부 트래픽이 1~2분 끊긴다. 이 정도는 감수할 수 있다고 판단했다. Incus OS에는 클러스터 기능도 있지만 이 구성에는 지나치다.
구축 과정에서 겪은 문제와 남은 작업
구축은 쉽지 않았다. cloud-init에서 sops를 쓰려 했지만 사용하려던 배포판에서 sops 패키지를 찾을 수 없어, 부팅 때 비밀 정보를 복호화하도록 sops 자체를 Podman 컨테이너로 실행했다. Incus ACL은 양방향으로 모두 거부하는 구조를 이해하기 쉽지 않았다. 인스턴스 둘 사이의 연결을 허용하려면 송신과 수신 양쪽에서 모두 허용해야 한다.
인터넷 접근이 프록시와 레지스트리 미러에 의존하므로 OpenTofu가 이 구성 요소들을 다른 것보다 먼저 실행하도록 순서를 정해야 했다. CrowdSec 바운서도 시작하려면 CrowdSec 로컬 API가 먼저 실행 중이어야 한다. CrowdSec 자체를 제대로 설정하는 일도 복잡했다.
NAT와 컨테이너가 있는 환경에서 요청의 원래 IP를 확인하는 문제는 haproxy와 PROXY 프로토콜을 알게 된 뒤 해결됐다. 여러 단계의 리버스 프록시와 최종 애플리케이션까지 HTTP 헤더를 맞추는 작업은 특히 PeerTube와 Nextcloud에서 어려웠고, 올바른 도메인을 사용하도록 해야 했다. 일부 애플리케이션은 서로 다른 두 기본 도메인을 잘 처리하지 못해 공개 전용 또는 VPN 전용으로 둘지 결정해야 했다.
현재 구성에 만족하고 있다. 가상 머신만 실행하는 경우보다 빠르고 메모리와 디스크를 효율적으로 쓰며, 모든 것이 자동 업데이트된다. OpenTofu와 cloud-init으로 변경하는 과정은 SSH로 파일 하나를 고치는 것보다 무겁지만 잘 작동한다. 재부팅 뒤 서비스가 돌아올지 걱정하거나 Ansible 플레이북 실행이 잘못될까 불안해할 필요가 줄었다.
여가 시간 7개월을 들여 구축하며 컨테이너와 cloud-init을 많이 배웠다. 다만 대부분에게는 과도한 설계일 수 있고, 집의 단일 서버는 고가용성 플랫폼이 아니다. 일회용 시스템, 기본 거부 네트워크, SSH 없는 관리 방식에 관심이 있다면 이 구성이 아이디어가 될 수 있다.
앞으로는 너무 많은 로그를 필터링하고 CrowdSec 시나리오를 늘리고 싶다. Alertmanager도 만족스럽지 않다. Ubuntu의 unattended-upgrades처럼 apt-get update와 upgrade를 자동 실행하는 방식에 의존하지 않도록 시스템 컨테이너 전체를 갱신하는 방법도 찾아야 한다. Incus OS를 도입하지 않아도 Incus는 Linux 배포판에서 서비스로 실행할 수 있어 시험해 볼 수 있다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요