왜 데몬 하드닝이 필요한가

대부분의 서비스 유닛은 별도 설정 없이 root 또는 광범위한 권한으로 실행된다. 이 상태에서 애플리케이션에 원격 코드 실행 취약점이 하나 생기면, 공격자는 곧바로 파일시스템 전체 쓰기, 커널 모듈 로드, 다른 프로세스 조작까지 확보한다. 하드닝의 목표는 "취약점이 터져도 그 프로세스가 할 수 있는 일 자체를 줄이는 것"이다. systemd는 이걸 유닛 파일의 몇 줄로 커널 수준(namespace, seccomp, capabilities)에서 강제한다. 애플리케이션 코드를 고치지 않아도 되는 게 핵심이다.

먼저 현재 상태를 측정한다

추측으로 옵션을 넣지 말고 systemd-analyze security로 노출 점수부터 확인한다. 0~10 척도로 위험 항목을 나열해 주므로, 무엇을 잠글지 우선순위가 잡힌다.

systemd-analyze security myapp.service
# UNIT           EXPOSURE PREDICATE
# ...            9.6      UNSAFE
# → PrivateTmp=, ProtectSystem=, NoNewPrivileges= 등 미설정 항목 표시

전용 사용자와 파일시스템 격리

가장 먼저 root 실행을 끊는다. DynamicUser=yes는 서비스 실행 시점에 임시 UID를 발급하고 종료 시 회수하므로 별도 계정 관리가 필요 없다. 상태 저장이 필요하면 StateDirectory=가 /var/lib/ 아래 소유권까지 자동 세팅한다.

[Service]
ExecStart=/usr/local/bin/myapp
DynamicUser=yes
StateDirectory=myapp

# 파일시스템 격리
ProtectSystem=strict     # / 전체를 읽기전용, 쓰기는 명시 경로만
ProtectHome=yes          # /home, /root, /run/user 접근 차단
PrivateTmp=yes           # 격리된 /tmp (다른 서비스와 공유 안 됨)
ReadWritePaths=/var/lib/myapp

ProtectSystem=strict는 /etc까지 읽기전용으로 만드니, 로그·소켓·데이터 경로는 ReadWritePaths=로 예외를 열어야 한다. 서비스가 시작 직후 "Read-only file system" 오류로 죽으면 대개 이 예외 누락이다.

커널 인터페이스와 권한 축소

대다수 웹 데몬은 커널을 직접 만질 일이 없다. 아래 옵션들로 위험한 시스템콜과 capability를 통째로 제거한다.

NoNewPrivileges=yes                 # setuid 바이너리로 권한 상승 불가
CapabilityBoundingSet=              # 모든 capability 제거
# 80번 포트가 필요하면 딱 하나만:
# AmbientCapabilities=CAP_NET_BIND_SERVICE
# CapabilityBoundingSet=CAP_NET_BIND_SERVICE

ProtectKernelTunables=yes           # /proc/sys, /sys 쓰기 차단
ProtectKernelModules=yes            # 모듈 로드 차단
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service    # 서비스에 필요한 콜만 허용
SystemCallFilter=~@privileged @resources

SystemCallFilter=@system-service는 대표적인 서버 워크로드 시스템콜 집합을 화이트리스트로 잡고, ~ 접두사로 특권·자원조작 계열을 다시 뺀다. seccomp 필터라 위반 시 프로세스가 SIGSYS로 즉시 종료된다.

옵션별 효과 정리

지시어막는 대상주의점
ProtectSystem=strict/etc·/usr 변조쓰기 경로 예외 필요
NoNewPrivileges=yes권한 상승sudo·setuid 도구 호출 불가
SystemCallFilter=@system-service위험 syscall과도하면 정상 기능도 차단
RestrictAddressFamilies비정상 소켓netlink 쓰면 추가 필요

적용과 검증 절차

드롭인 파일로 원본 유닛을 건드리지 않고 얹는 방식을 권장한다. 롤백과 diff가 쉬워진다.

systemctl edit myapp.service   # /etc/systemd/system/myapp.service.d/hardening.conf
systemctl daemon-reload
systemctl restart myapp.service
journalctl -u myapp.service -f    # SIGSYS·EPERM·Read-only 오류 관찰

한 번에 모든 옵션을 켜지 말고, 격리 → 권한 → seccomp 순으로 단계별로 재시작하며 로그를 본다. 특히 SystemCallFilter는 라이브러리 업데이트로 새 syscall이 생기면 조용히 죽을 수 있으니, 배포 파이프라인에 systemd-analyze security 점수 체크를 넣어 회귀를 잡는 게 안전하다.

주의할 점

하드닝은 만능이 아니다. 애플리케이션이 실제로 쓰는 경로·소켓·시스템콜을 모르면 과잉 차단으로 장애를 만든다. 스테이징에서 strace나 seccomp 로그로 실제 사용 범위를 먼저 파악하고, 컨테이너 안에서 도는 서비스라면 일부 옵션이 런타임과 중복되거나 충돌할 수 있음을 확인해야 한다. 목표는 "동작하는 최소 권한"이지 "옵션을 최대한 많이 켜기"가 아니다.