NixOS에서 systemd 및 서비스 하드닝, journald 보호 방법 등 현대 리눅스 시스템 보안 강화를 위한 실무적 가이드입니다.
리눅스 시스템은 각 구성 요소마다 고유의 취약점을 지닌 복잡한 생태계입니다. NixOS도 예외는 아닙니다. 선언적 방식이 일부 이점을 제공하긴 하지만, 내재된 불안정성을 완전히 막아주는 은탄환은 될 수 없습니다. 이 불안전한 상태는 언제나 존재하며, 이는 우리에게 행동을 촉구합니다.
지난 6개월간, 저는 NixOS 설치의 모든 구성 요소를 하나씩 하드닝하는 데 집중해 왔습니다. 본 글은 그 과정의 기준점 마련 및 경험 기록을 위한 시리즈의 첫 번째로, systemd 하드닝에 대해 다룹니다. NixOS는 systemd 기반 배포판이므로 systemd부터 시작합니다. 앞으로 커널 및 네트워크 보안도 다룰 예정이나, 본 포스트에서는 systemd에만 집중합니다.
이번 편에서는 시스템에서 systemd 서비스를 어떻게 하드닝하여 공격 표면을 줄일 수 있는지 살펴봅니다.
systemd는 현대 리눅스 배포판의 핵심으로, 시스템 관리를 간소화하는 다양한 기능을 제공합니다. 그러나 중앙집중적 구조로 인해 단일 실패지점이 될 수 있고, 방대한 기능은 오히려 잘못 설정될 시 공격 표면을 넓히기도 합니다. 오늘 유용하게 쓸 수 있는 도구 중 하나가 systemd-analyze security 입니다.
sudo systemd-analyze security
를 실행해보세요. "UNSAFE" 또는 "EXPOSED"로 표시되는 서비스가 많음을 금방 알게 될 것입니다. 우선 걱정하지 마세요. 시스템이 실제로 위험한 건 아닙니다. Systemd의 평가는 전적으로 임의적입니다. 단순히 설정을 기반으로 점수를 매기는 규칙성 분석일 뿐, 실제 취약점의 존재 여부를 판단해주지 못합니다. 각 서비스의 점수는 실제 보안의 척도가 아니지만, 시작점은 됩니다. Service 하드닝은 실행 파일 자체에 취약점이 있을 때를 대비한 2차 방어선입니다.
systemd-analyze security <unit>
은 각 유닛별로 점수와 적용된 directive를 보여줍니다. 완벽하진 않지만, 이를 바탕으로 개별 서비스의 하드닝을 시도할 수 있습니다.
기본적으로 systemd는 실행 중인 서비스를 안전하지 않은 상태로 둡니다. 이는 서비스 실행에 사전에 제약을 두면 충돌이 발생할 수 있기 때문입니다.
NixOS도 나름 서비스를 하드닝하려 시도하지만, 실제로는 NixOS 서비스이든 패키지 매니저로 설치하든 별다른 하드닝 없이 실행되는 경우가 많습니다. 이 글은 경험을 바탕으로 하드닝 가능한 옵션을 정리한 것입니다. 다음의 두 가지 원칙을 꼭 기억하세요.
일부 하드닝 옵션은 특정 경로의 접근을 막거나 읽기 전용으로 만듭니다. 이론상으론 도움이 되지만, 프로그램이 비정상적으로 종료될 수 있으므로 한 서비스씩 신중하게 적용해야 합니다.
NixOS의 systemd 서비스는 Nixpkgs 모듈 시스템의 Systemd 모듈로 정의합니다. 기본적으로 [Service]
필드만 다뤄도 충분합니다. 추가 문서는 systemd.unit(5)
, systemd.service(5)
, systemd.exec(5)
매뉴얼 페이지를 참고하면 좋습니다.
NixOS에서 services.*
로 활성화하는 대부분의 서비스는 사실상 systemd.services.<name>
의 추상화입니다. 서비스의 하드닝은 대개 services.*
옵션만으론 한계가 있으므로, 직접 systemd 서비스의 serviceConfig
에서 변경해줘야 합니다. 단일 모듈에서 여러 서비스를 만드는 경우도 있으니 systemctl list-units --type=service
로 실제 유닛을 확인하세요.
아래는 옵션 적용 예시입니다.
nix{ systemd.services."<serviceName>".serviceConfig = { ProtectClock = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; SystemCallFilter = "~@clock @cpu-emulation @debug @obsolete @module @mount @raw-io @reboot @swap"; ProtectControlGroups = true; RestrictNamespaces = true; LockPersonality = true; MemoryDenyWriteExecute = true; RestrictRealtime = true; RestrictSUIDSGID = true; }; }
위의 옵션만 적용해도 서비스의 노출 수준은 MEDIUM 정도로 낮아집니다. 불필요한 권한(실은 필요한 경우도 있음)을 제거하는 효과가 있죠. 하지만 이 예시는 최소화된 예시이므로, 시스템 자원을 많이 쓰는 서비스에선 필요에 맞게 권한 조정을 해줘야 합니다. 서비스 하드닝 후에는 systemd-analyze security <unit>
로 다시 점검하세요.
위 예시 기준으로, 자주 쓰이는 directive는 다음과 같습니다.
옵션 | 설명 |
---|---|
ProtectClock | 서비스가 시스템 시계에 접근하지 못하도록 함 |
ProtectKernelTunables | 커널 파라미터 수정 제한 |
ProtectKernelModules | 커널 모듈 로딩 방지 |
ProtectKernelLogs | 커널 로그 접근 제한 |
ProtectHome | /home, /root, /run/user를 읽기 전용 tmpfs로 마운트 |
ProtectSystem | /boot , /etc , /usr 를 읽기 전용으로 마운트 |
SystemCallFilter | 특정 시스템 콜 필터링 |
ProtectControlGroups | control group 사용 제한 |
RestrictNamespaces | 프로세스 namespace 제한 |
LockPersonality | 프로세스 personality 변경 방지 |
MemoryDenyWriteExecute | 실행/쓰기 동시 권한 메모리 매핑 금지 |
RestrictRealtime | 실시간 스케줄링 권한 제한 |
RestrictSUIDSGID | SUID/SGID 바이너리 제한1 |
이 외에도 다양한 옵션이 있으나, 위의 옵션들은 서비스 하드닝에 큰 무리 없이 적용할 수 있는 범용 옵션입니다.
Archwiki의 Systemd Sandboxing 초안에서 더 많은 옵션과 영향도를 참고하세요.
아래는 추가로 알아두면 좋은 몇 가지 directive입니다.
서비스에서 반드시 숨겨야 할 디렉터리에 직접 접근 권한을 제거하고 싶을 때 InaccessiblePaths
옵션을 사용하세요.
systemd system 서비스는 별도 지정이 없으면 root 권한으로 실행됩니다. DynamicUsers
옵션을 사용하면 서비스별로 임시 유저를 동적으로 생성하여 각 인스턴스를 분리 실행할 수 있습니다. 이렇게 하면 프로세스 간 격리가 이루어져 보안성이 올라갑니다. root 권한이 필요 없는 서비스라면 꼭 고려해볼 옵션입니다.
SystemCallFilter
는 서비스가 사용할 수 있는 시스템 콜을 제한합니다. 관리가 쉽지 않지만, systemd는 @
접두사가 붙은 콜 그룹을 제공합니다(@swap
, @reboot
등). 자세한 내용은 linux-audit.com의 설명을 참고하세요.
systemd 235부터 개별 서비스의 네트워크 트래픽 통계를 기록할 수 있게 되었습니다. IPAddressAllow
와 IPAddressDeny
directive로 네트워크 리소스 접근을 제한할 수 있습니다. 자세한 내용은 IP Accounting 관련 블로그를 참고하세요.
실제 적용 시, systemd의 에러 메시지는 친절하지 않을 수 있습니다. 로그를 디버그로 올리려면 systemctl log-level debug
를 사용하세요.2
서비스별로 systemd-analyze security <unit>
을 이용해 하드닝이 필요한 서비스를 확인하고, 아래처럼 하드닝을 적용할 수 있습니다.
예) systemd-analyze security acpid
결과 예시:
✗ RootDirectory=/RootImage= 서비스가 호스트의 루트 디렉토리 내에서 실행됨 0.1
SupplementaryGroups= 서비스가 root이므로 무시
RemoveIPC= 서비스가 root이므로 무시
✗ User=/DynamicUser= 서비스가 root 계정으로 실행됨 0.4
✗ CapabilityBoundingSet=~CAP_SYS_TIME 시간 변경 권한 있음 0.2
✗ NoNewPrivileges= 서비스가 새로운 권한을 획득할 수 있음 0.2
✓ AmbientCapabilities= 프로세스가 ambient capability를 받지 않음
✗ PrivateDevices= 서비스가 하드웨어 디바이스에 접근 가능 0.2
✗ ProtectClock= 서비스가 시계에 쓸 수 있음 0.2
...
이런 방식으로 각 서비스의 문제점을 파악하여 하드닝 적용 여부를 점검하세요. 저는 50여 개의 systemd 서비스 중 상당수를 EXPOSED, UNSAFE, MEDIUM 등급에서 직접 하드닝으로 낮췄습니다.3 3rd party 하드닝 프로젝트도 참고할 수 있으나, 모든 시스템에 맞는 일괄대응책은 존재하지 않습니다. 신뢰할 수 있는 소스가 아니라면 의존하지 않는 게 보안상 안전합니다.
krathalan의 systemd 샌드박싱 템플릿은 기초 작업에 큰 도움이 됩니다. 각종 서비스 템플릿과 옵션/시스템간 상호작용 설명이 있어 유용하게 쓸 수 있습니다. 단, 하드닝 시 항상 시스템에 최소 한 개의 정상 부팅 가능한 generation이 남아 있게 하세요.
아래는 높은 노출 점수를 받는 서비스를 위한 실무적 하드닝 예시입니다.
nix{ systemd.services.acpid.serviceConfig = { ProtectSystem = "full"; ProtectHome = true; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; SystemCallFilter = "~@clock @cpu-emulation @debug @module @mount @raw-io @reboot @swap"; ProtectKernelTunables = true; ProtectKernelModules = true; }; }
nix{ systemd.services.power-profiles-daemon.serviceConfig = { ProtectHome = true; ProtectClock = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; SystemCallFilter = "~@clock @cpu-emulation @debug @obsolete @module @mount @swap"; ProtectControlGroups = true; RestrictNamespaces = true; LockPersonality = true; MemoryDenyWriteExecute = true; RestrictRealtime = true; RestrictSUIDSGID = true; }; }
Systemd 서비스들이 대체로 느슨한 권한으로 실행된다는 점은 널리 알려지만, systemd의 로그 시스템인 Journald 역시 간과하기 쉽습니다.
Journald는 시스템 및 애플리케이션 데이터를 방대하게 수집합니다. 이 정보가 보호되지 않으면 심각한 보안 위협이 될 수 있습니다. 따라서 시스템 로그 하드닝은 시스템 무결성 및 민감 정보 보호, 보안 규정 준수를 위해 필수입니다.
분포판별로 Journald 설정이 조금씩 다르므로, 일반 원칙은 암호화 저장소에 저널 파일을 저장하고, 파일 권한을 640
등 최소한으로 제한하는 것입니다. 네트워크로 로그 전송 시엔 반드시 암호화가 필요하며, 로그 파일은 신중하게 다뤄야 합니다.
다음과 같이 volatile(메모리) 저장으로 Journald 하드닝을 시작할 수 있습니다.
nix{ services.journald = { storage = "volatile"; # 로그를 메모리에 저장 upload.enable = false; # 원격 전송 비활성화(기본값) }; }
옵션 전체 목록은 man 5 journald.conf
를 참고해 services.journald.extraConfig
에 적용할 수 있습니다.
또한, 서비스 로그가 저널로 대량 전송될 경우 디스크 용량 소모로 서비스 거부 공격에 노출될 수 있으므로, SystemMaxUse
옵션으로 용량 제한을 두는 것이 좋습니다.
서비스 하드닝만으로 완벽한 보안이 구현되지는 않습니다. Systemd의 노출 점수 역시 절대적 기준은 아닙니다. 각 서비스의 필요성과 공격 벡터를 반드시 고려하세요.
아래는 본 글 작성 시 참고한 리소스입니다. 대부분 Systemd 하드닝을 넘어선 깊이를 다루므로, 심화 학습 및 후속 포스트에서도 소개할 계획입니다.
man 5 systemd.exec
위협 모델 없는 보안이라는 개념은 없습니다.
SUID(설정된 사용자 ID)와 GUID(설정된 그룹 ID) 바이너리는 실행될 때 파일 속성상 소유자나 그룹으로 권한을 임시 승격합니다. 이는 설정 변경이나 제한 파일 접근 등 루트 권한 작업용이지만, 올바로 구현되지 않으면 오용될 소지가 있으므로 RestrictSUIDSGID
같은 옵션으로 제한해야 합니다. ↩
모든 서비스를 완전히 하드닝하는 것은 현실적으로 불가능합니다. 일부 서비스는 Systemd 기준상 항상 UNSAFE 상태일 수 있습니다(예: root로 동작하는 서비스). root 계정을 별도 사용자로 교체하거나 DynamicUser를 쓰면 기능이 깨질 수 있으므로, 일부 서비스는 노출이 불가피합니다. ↩