서비스 · 프로세스 관리 38 / 50 · Part 4. 로그·스케줄링·운영
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

30편에서 서비스를 root가 아닌 계정으로 실행하는 것만으로도 피해 범위가 크게 줄어든다는 것을 확인했다. 그러나 전용 계정이라도 읽을 수 있는 파일은 모두 읽고, 쓸 수 있는 곳에는 쓸 수 있다. 웹 앱이 탈취되면 공격자는 그 계정으로 /home을 뒤지고, /tmp에 파일을 두고, 권한 상승을 시도할 수 있다.

systemd는 이런 행동을 서비스 단위로 막는 샌드박스 옵션 을 제공한다. 이번 글에서는 systemd-analyze security로 25편의 labapp 서비스가 얼마나 노출되어 있는지 측정하고, drop-in 하나로 샌드박스를 적용해 점수와 실제 동작이 어떻게 바뀌는지 확인한다.


2. 핵심 개념

2-1. 주요 샌드박스 옵션 ([Service])

분류설정효과
파일 시스템ProtectSystem=strict/ 전체 읽기 전용 (full은 /usr·/boot·/etc만)
ProtectHome=yes/home, /root, /run/user 숨김
PrivateTmp=yes서비스 전용 /tmp, /var/tmp
ReadWritePaths=strict 상태에서 쓰기를 허용할 경로
StateDirectory= / LogsDirectory=서비스 전용 쓰기 디렉터리를 자동 생성
권한NoNewPrivileges=yesSUID·파일 Capability로 권한을 늘릴 수 없음
CapabilityBoundingSet=가질 수 있는 Capability 상한 (30편)
장치·커널PrivateDevices=yes실제 장치 접근 차단
ProtectKernelTunables= / ProtectKernelModules=/proc/sys 쓰기, 모듈 적재 차단
ProtectControlGroups=yescgroup 수정 차단
네트워크·시스템 콜RestrictAddressFamilies=허용 소켓 종류 제한
SystemCallFilter=@system-service일반 서비스용 시스템 콜 목록만 허용
RestrictNamespaces=yes새 네임스페이스 생성 차단

2-2. systemd-analyze security

서비스의 샌드박스 설정을 항목별로 평가해 0.0(가장 안전)~10.0(가장 노출) 점수를 매긴다. ✗ 표시된 항목이 개선 후보다. 점수는 설정 기준의 평가이므로, 애플리케이션 자체의 취약점과는 별개다.


3. 동작 원리

샌드박스 적용 전후 — labapp 노출 점수 9.2 → 2.4

systemctl restart labapp (샌드박스 drop-in 적용)
  → PID 1 fork → 자식에서 exec 전에
       새 마운트 네임스페이스 생성
       / 를 읽기 전용으로 다시 마운트 (ProtectSystem=strict)
       /home 을 빈 디렉터리로 가림 (ProtectHome)
       전용 /tmp 를 /tmp/systemd-private-*/ 로 연결 (PrivateTmp)
       no_new_privs 플래그 설정 (NoNewPrivileges)
       Capability 제거 · seccomp 필터 적용
  → execve(python3 app.py)

이 모든 제한은 커널 기능(네임스페이스·seccomp·Capability) 으로 구현되며, 앱 코드는 전혀 바뀌지 않는다. 설정이 커널에서 지원되지 않으면 서비스가 226/NAMESPACE 등으로 시작 실패하므로(24편), 적용 후 반드시 동작을 확인한다.


4. 명령어 실습

# 1) 적용 전 노출 점수와 실제 접근 범위
systemd-analyze security labapp.service --no-pager | tail -3
systemd-analyze security labapp.service --no-pager | grep -E '✗' | head -10
systemd-run --wait -p User=analyst --pipe /bin/bash -c 'ls /home; touch /tmp/probe && echo "/tmp 쓰기 가능"; cat /etc/hostname'

# 2) 샌드박스 drop-in 적용
cat > /etc/systemd/system/labapp.service.d/50-sandbox.conf <<'EOF'
[Service]
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
LockPersonality=yes
CapabilityBoundingSet=
SystemCallFilter=@system-service
EOF
systemctl daemon-reload; systemctl restart labapp; systemctl is-active labapp
systemd-analyze security labapp.service --no-pager | tail -1
python3 -c 'import urllib.request as u; print("HTTP", u.urlopen("http://127.0.0.1:8082/").status)'

# 3) 샌드박스 안에서 본 세상
systemd-run --wait --pipe -p User=analyst -p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes -p NoNewPrivileges=yes \
  /bin/bash -c 'ls /home; touch /tmp/probe && echo "/tmp 쓰기 가능(격리된 tmp)"; ls /tmp; touch /opt/x; sudo -n id | head -1'
ls /tmp | grep -c systemd-private

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 샌드박스 없는 서비스의 노출 점수

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 샌드박싱 drop-in 적용

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 샌드박스 안에서 본 파일 시스템

텍스트 원본(실제 출력):

[root@rocky9-lab ~]# systemd-analyze security labapp.service --no-pager | tail -3
✗ UMask=                                                      Files created by service are world-readable by default                       0.1

→ Overall exposure level for labapp.service: 9.2 UNSAFE 😨
[root@rocky9-lab ~]# systemd-analyze security labapp.service --no-pager | grep -E '✗' | head -10
✗ RemoveIPC=                                                  Service user may leave SysV IPC objects around                               0.1
✗ RootDirectory=/RootImage=                                   Service runs within the host's root directory                                0.1
✗ CapabilityBoundingSet=~CAP_SYS_TIME                         Service processes may change the system clock                                0.2
✗ NoNewPrivileges=                                            Service processes may acquire new privileges                                 0.2
✗ PrivateDevices=                                             Service potentially has access to hardware devices                           0.2
✗ ProtectClock=                                               Service may write to the hardware clock or system clock                      0.2
✗ CapabilityBoundingSet=~CAP_SYS_PACCT                        Service may use acct()                                                       0.1
✗ CapabilityBoundingSet=~CAP_KILL                             Service may send UNIX signals to arbitrary processes                         0.1
✗ ProtectKernelLogs=                                          Service may read from or write to the kernel log ring buffer                 0.2
✗ CapabilityBoundingSet=~CAP_WAKE_ALARM                       Service may program timers that wake up the system                           0.1
[root@rocky9-lab ~]# systemd-run --wait -p User=analyst --pipe /bin/bash -c 'ls /home; touch /tmp/probe && echo "/tmp 쓰기 가능"; cat /etc/hostname' 2>&1
Running as unit: run-u306.service
analyst
/tmp 쓰기 가능
rocky9-lab
Finished with result: success
Main processes terminated with: code=exited/status=0
Service runtime: 14ms
[root@rocky9-lab ~]# cat > /etc/systemd/system/labapp.service.d/50-sandbox.conf <<'EOF'
> [Service]
> NoNewPrivileges=yes
> ProtectSystem=strict
> ProtectHome=yes
> PrivateTmp=yes
> PrivateDevices=yes
> ProtectKernelTunables=yes
> ProtectKernelModules=yes
> ProtectControlGroups=yes
> RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
> RestrictNamespaces=yes
> LockPersonality=yes
> CapabilityBoundingSet=
> SystemCallFilter=@system-service
> EOF
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl restart labapp; sleep 1; systemctl is-active labapp
active
[root@rocky9-lab ~]# systemd-analyze security labapp.service --no-pager | tail -1
→ Overall exposure level for labapp.service: 2.4 OK 🙂
[root@rocky9-lab ~]# python3 -c 'import urllib.request as u; print("HTTP", u.urlopen("http://127.0.0.1:8082/").status)'
HTTP 200
[root@rocky9-lab ~]# systemd-run --wait --pipe -p User=analyst -p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes -p NoNewPrivileges=yes /bin/bash -c 'ls /home; touch /tmp/probe && echo "/tmp 쓰기 가능(격리된 tmp)"; ls /tmp; touch /opt/x 2>&1; sudo -n id 2>&1 | head -1' 2>&1
Running as unit: run-u311.service
ls: cannot open directory '/home': Permission denied
/tmp 쓰기 가능(격리된 tmp)
probe
touch: cannot touch '/opt/x': Read-only file system
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
Finished with result: success
Main processes terminated with: code=exited/status=0
Service runtime: 26ms
[root@rocky9-lab ~]# ls /tmp | grep -c systemd-private
4

6. 결과 해석

관찰의미
적용 전 Overall exposure level ... 9.2 UNSAFEUser=만 지정한 서비스도 설정 기준으로는 거의 무방비다
✗ NoNewPrivileges=, PrivateDevices=, ProtectClock=, CapabilityBoundingSet=~CAP_KILL ...개선할 항목과 각 항목의 가중치가 표시된다
샌드박스 없는 analyst: /home 목록, /tmp 쓰기, /etc/hostname 읽기 성공전용 계정이라도 파일 권한이 허용하는 범위는 모두 접근한다
drop-in 적용 후 active, 점수 2.4 OK설정 파일 하나로 노출 점수가 크게 낮아졌다
HTTP 200웹 앱 기능은 그대로 동작한다. 샌드박싱은 기능을 유지하면서 불필요한 권한만 제거 한다
샌드박스 안 ls /home → Permission deniedProtectHome이 사용자 홈을 가렸다
/tmp 쓰기 가능, ls /tmp → probe만PrivateTmp로 서비스 전용 /tmp 를 받았다. 다른 서비스나 사용자의 파일은 보이지 않는다
touch /opt/x → Read-only file systemProtectSystem=strict로 전체가 읽기 전용이다
sudo → "no new privileges" 거부NoNewPrivileges가 SUID 실행을 통한 권한 상승을 막았다
호스트 /tmp의 systemd-private-* 4개PrivateTmp 서비스들이 실제로 쓰는 디렉터리. 서비스별로 분리되어 있다

7. 보안 관점

주제내용
피해 범위 축소웹 앱 취약점으로 명령 실행을 당해도 홈 디렉터리 열람, 시스템 파일 변조, 권한 상승 시도가 커널 수준에서 차단 된다
우선 적용 대상외부에 노출된 서비스(웹, API, 파일 업로드 처리), 사용자 입력을 파싱하는 서비스
점수의 한계점수는 "설정이 얼마나 촘촘한가"다. 점수가 낮아도 앱 취약점·자격 증명 노출은 별도로 관리해야 한다
배포판 기본값최근 배포판의 시스템 서비스(journald, logind, resolved 등)는 이미 샌드박스를 적용하고 있다. systemd-analyze security(인자 없이)로 전체 서비스를 비교할 수 있다
설정 해제 감시28편처럼 drop-in으로 샌드박스를 끄는 변경 도 가능하다. systemd-delta로 변경을 감시한다

8. 보안관제 관점

[정기 점검]  systemd-analyze security --no-pager | sort -k2 -n -r | head -15
             → 노출 점수가 높은 서비스 목록 (EXPOSURE, PREDICATE)
     ↓
[우선순위]   외부 포트를 연 서비스(16편) ∩ 점수 높은 서비스 → 개선 권고 1순위
     ↓
[개선]       drop-in 으로 옵션 추가 → 기능 테스트 → 점수 재측정 → 변경 이력 기록
     ↓
[변경 감시]  systemd-delta --type=extended   ← 샌드박스 drop-in 이 제거·완화되지 않았는가
점검 명령목적
systemd-analyze security전체 서비스 노출 점수
systemd-analyze security UNIT항목별 상세
systemctl show UNIT -p ProtectSystem,NoNewPrivileges,PrivateTmp실제 적용 값

9. 실무에서 자주 발생하는 실수

실수결과예방
ProtectSystem=strict 후 로그·데이터 쓰기 실패서비스 오류StateDirectory=, LogsDirectory=, ReadWritePaths=
ProtectHome=yes인데 앱이 /home 아래에 있음실행 실패앱을 /opt·/srv로, 또는 read-only
SystemCallFilter를 과하게 좁힘특정 기능에서 SIGSYS 종료@system-service부터 시작, 로그로 조정
점수만 보고 적용 후 테스트 생략운영 장애기능 테스트 필수
모든 옵션을 한 번에 적용원인 파악 어려움단계적으로 적용

10. 실습 체크리스트

[ ] systemd-analyze security 로 노출 점수를 측정했다
[ ] 샌드박스 없는 계정의 접근 범위를 확인했다
[ ] drop-in 으로 샌드박스 옵션을 적용했다
[ ] 점수가 9.2 → 2.4 로 낮아지고 기능은 유지되는 것을 확인했다
[ ] ProtectHome, PrivateTmp, ProtectSystem, NoNewPrivileges 효과를 직접 확인했다
[ ] 샌드박스 완화 변경을 systemd-delta 로 감시할 수 있다

11. 핵심 정리

  • 샌드박싱은 서비스가 탈취되었을 때 할 수 있는 일을 커널 기능으로 제한 한다.
  • ProtectSystem, ProtectHome, PrivateTmp, NoNewPrivileges, CapabilityBoundingSet이 핵심 옵션이다.
  • systemd-analyze security로 노출 점수를 측정하고 개선 항목을 찾는다 (실측 9.2 → 2.4).
  • 앱 코드 수정 없이 drop-in으로 적용할 수 있지만, 쓰기 경로를 따로 열어 주고 기능 테스트를 해야 한다.
  • 샌드박스 설정이 완화되는 변경도 감시 대상이다.

12. 다음 편 예고

다음 글 「39. 부팅 시 자동 실행 경로 총정리」 에서는 지금까지 다룬 systemd unit, timer, cron, at, 셸 초기화 파일을 한데 모아 "이 서버에서 자동으로 실행되는 것" 전체 목록 을 뽑는 점검 스크립트를 만든다. 무해한 항목을 직접 심어 두고 스크립트가 모두 찾아내는지 확인한다.


참고 자료


시리즈 이동

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글