대상 OS: Debian 12 (bookworm)
운영 서버에서 ‘취약점 패치’만큼이나 중요한 게 “침해 당했을 때의 폭발 반경(blast radius)을 얼마나 줄이느냐”입니다. Debian 12는 기본 init이 systemd이기 때문에, 별도 컨테이너를 쓰지 않더라도 **서비스 단위로 샌드박싱/권한 축소를 상당히 강하게** 걸 수 있습니다.
이 글은 nginx를 예시로 하지만, 원리는 대부분의 데몬(웹/에이전트/배치/내부 API)에 동일하게 적용됩니다.
---
0) 오늘 목표
- systemd drop-in(override.conf)로 서비스 권한을 축소한다.
- 적용 전/후를 `systemd-analyze security`로 점수화해 확인한다.
- 문제 생기면 즉시 롤백할 수 있게 한다.
---
1) 현재 서비스 상태/취약 지점 빠른 확인
먼저 “현재 서비스가 어떤 권한으로 무엇에 접근하는지”를 빠르게 봅니다.
sudo systemctl status nginx --no-pager
sudo systemctl cat nginx
sudo systemd-analyze security nginx --no-pager
`systemd-analyze security nginx`는 각 보호 옵션이 꺼져있으면 경고를 띄우고, 대략적인 “노출 점수”를 매깁니다. 운영 중에는 이 출력을 변경 전/후로 저장해두면 좋습니다.
sudo systemd-analyze security nginx --no-pager | sed -n '1,120p'
---
2) Debian 12에서 nginx 기본 경로(권한 설계의 출발점)
하드닝을 걸기 전에, nginx가 실제로 쓰는 파일/디렉터리를 정리해야 합니다. (여길 대충 잡으면 100% 서비스가 죽습니다.)
보통 Debian 12 nginx 패키지 기준으로:
- 설정: `/etc/nginx/`
- 웹 루트(예시): `/var/www/html/`
- 로그: `/var/log/nginx/`
- 런타임/캐시/임시: `/var/lib/nginx/`, `/run/nginx.pid` 등
실제 접근을 확인하려면 `strace`까지 갈 수도 있지만, 간단히는 다음을 먼저 봅니다.
sudo nginx -T 2>/dev/null | sed -n '1,120p'
sudo grep -R "access_log\|error_log\|client_body_temp_path\|proxy_temp_path\|fastcgi_temp_path\|uwsgi_temp_path\|scgi_temp_path" -n /etc/nginx/nginx.conf /etc/nginx/conf.d /etc/nginx/sites-enabled 2>/dev/null
---
3) Drop-in으로 하드닝 적용(핵심)
`systemctl edit`를 쓰면 패키지 업데이트 시에도 안전하게 override를 유지할 수 있습니다.
1) drop-in 파일을 연다.
sudo systemctl edit nginx
2) 아래 내용을 붙여 넣는다(상황에 따라 주석 해제/조정).
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
# (A) 파일시스템/권한 격리
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
# nginx가 꼭 써야 하는 경로만 예외로 열어준다.
# (운영 환경에 따라 경로는 반드시 확인)
ReadWritePaths=/var/log/nginx /var/lib/nginx /run
# (B) 커널 인터페이스/장치 접근 최소화
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
PrivateDevices=true
LockPersonality=true
# (C) 네임스페이스/IPC 제한
RestrictNamespaces=true
RemoveIPC=true
# (D) 시스템 콜/주소공간 방어
MemoryDenyWriteExecute=true
RestrictSUIDSGID=true
# (E) 네트워크 계열은 막으면 안 되므로, 대신 주소 패밀리만 제한(필요한 것만)
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# (F) 위험 capability를 통째로 주지 않는다(기본 nginx는 root로 바인드 후 worker로 드랍)
CapabilityBoundingSet=
AmbientCapabilities=
# (G) 추가로 강하게 걸고 싶다면(환경에 따라 깨질 수 있어 단계적으로)
# SystemCallFilter=@system-service
# SystemCallErrorNumber=EPERM
여기서 중요한 포인트는 `ProtectSystem=strict`와 `ReadWritePaths=` 조합입니다.
- `ProtectSystem=strict`는 사실상 루트 파일시스템을 읽기 전용으로 만들어버립니다.
- 그래서 nginx가 “반드시 써야 하는 디렉터리(로그, 캐시, pid)”만 `ReadWritePaths`로 열어줘야 합니다.
---
4) 설정 반영 → 즉시 검증
1) 데몬 리로드 및 재시작.
sudo systemctl daemon-reload
sudo systemctl restart nginx
2) 에러 로그 확인(대부분의 실패는 권한/쓰기 경로입니다).
sudo journalctl -u nginx -b --no-pager -n 200
sudo tail -n 200 /var/log/nginx/error.log
3) 점수 비교.
sudo systemd-analyze security nginx --no-pager
이 단계에서 점수가 확 내려가면(=보호 옵션이 켜지면) 목적의 80%는 달성입니다.
---
5) 자주 터지는 문제 5가지(그리고 빠른 처방)
1) 로그 파일을 못 쓴다(permission denied).
sudo ls -ld /var/log/nginx
sudo namei -l /var/log/nginx
- `ProtectSystem=strict` + `ReadWritePaths`에 `/var/log/nginx`가 빠졌거나,
- 디렉터리 소유권/권한이 nginx worker 사용자(`www-data`)와 안 맞는 경우가 많습니다.
2) 업로드/프록시 임시파일을 못 만든다.
nginx 설정의 temp path가 `/var/lib/nginx/...` 바깥을 가리키면 막힙니다. temp 경로를 `/var/lib/nginx` 아래로 모으고 `ReadWritePaths`에 포함시키세요.
sudo mkdir -p /var/lib/nginx/body /var/lib/nginx/proxy
sudo chown -R www-data:www-data /var/lib/nginx
3) PID 파일/런타임 디렉터리 문제.
- `/run`은 tmpfs로 재부팅마다 초기화됩니다.
- nginx가 `/run/nginx.pid`를 쓰는 구조면 `/run`을 쓰기 가능으로 열어야 합니다(override 예시에 포함).
sudo ls -ld /run
sudo systemctl show nginx -p PIDFile
4) `MemoryDenyWriteExecute=true`에서 모듈/외부 확장과 충돌.
대부분의 표준 패키지 nginx는 문제 없지만, 특이한 동적 모듈/서드파티가 JIT/메모리 실행을 요구하면 깨질 수 있습니다. 이 옵션은 강력한 대신 호환성 이슈가 있을 수 있으니 장애 시 우선 주석 처리 후 원인 분석을 권합니다.
5) 너무 욕심내서 `SystemCallFilter`로 바로 막았다가 서비스가 죽는다.
`SystemCallFilter=@system-service`는 꽤 강합니다. 운영에서는 “1) 파일시스템 격리 → 2) 커널/디바이스 격리 → 3) syscall 필터” 순으로 단계적으로 넣는 게 안전합니다.
---
6) 롤백(진짜 중요)
하드닝은 보안 개선이지만, 동시에 “가용성 리스크”이기도 합니다. 롤백이 10초 안에 되도록 해두세요.
1) drop-in을 제거(또는 비움)하고 재시작.
sudo systemctl revert nginx
sudo systemctl restart nginx
`revert`는 override를 되돌립니다. 일부 환경에서는 drop-in만 지우고 싶을 수 있으니, 실제 파일 위치도 기억해두면 좋습니다.
sudo ls -l /etc/systemd/system/nginx.service.d/
---
7) 운영 팁: “서비스별 보안 등급”을 만들어라
한 번에 모든 데몬을 컨테이너로 옮기지 못하는 경우가 대부분입니다. 대신 systemd 하드닝으로 다음처럼 등급을 나누면 현실적으로 관리가 됩니다.
1) Tier-1(외부 노출: 웹/프록시).
- `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true` 필수
- 쓰기 경로 최소화(`ReadWritePaths`)
- 주소 패밀리 제한(`RestrictAddressFamilies`)
2) Tier-2(내부 API/에이전트).
- Tier-1 + `RestrictNamespaces=true`, `PrivateDevices=true`
- 가능하면 `DynamicUser=true`(단, 상태 저장 경로/권한 설계가 필요)
3) Tier-3(배치/관리 도구).
- 위험 옵션(특권 작업 필요)은 예외로 두되,
- 실행 시점/권한 상승 경로(sudoers)와 로그를 더 빡세게 남기기
마지막으로, 하드닝은 “한 번 켜고 끝”이 아니라 배포/변경 때마다 점검해야 하는 항목입니다. `systemd-analyze security` 결과를 CI 체크리스트에 붙여두면, 시간이 지나도 방치되지 않습니다.