서비스 · 프로세스 관리 28 / 50 · Part 3. systemd와 서비스
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

nginx의 파일 한도를 올리거나 서비스에 환경변수 하나를 추가하려고 /usr/lib/systemd/system/nginx.service를 직접 고쳤다. 몇 주 뒤 패키지 업데이트가 설치되자 변경 사항이 사라지고 장애가 재발했다. 패키지가 원본 파일을 덮어썼기 때문이다.

systemd는 이 문제를 drop-in 으로 해결한다. 원본은 그대로 두고, 서비스명.service.d/ 디렉터리에 바꿀 부분만 담은 .conf 파일 을 두면 systemd가 합쳐서 적용한다. 이번 글에서는 drop-in으로 설정을 바꾸고, 특히 자주 실수하는 ExecStart 교체 규칙 을 실패 로그로 확인한다.


2. 핵심 개념

2-1. drop-in 위치와 적용

항목내용
위치/etc/systemd/system/NAME.service.d/*.conf (관리자), /usr/lib/systemd/system/NAME.service.d/ (패키지), /run/...
적용 순서메인 unit → drop-in 파일명 알파벳 순 (같은 이름이면 /etc 우선)
공통 drop-inservice.d/(모든 서비스), foo-.service.d/(foo-로 시작하는 서비스)
만드는 법systemctl edit NAME (편집기로 override.conf 생성·daemon-reload 자동) 또는 직접 파일 작성 후 daemon-reload
전체 교체systemctl edit --full NAME → /etc에 unit 전체 복사본 생성
되돌리기systemctl revert NAME → /etc의 override·복사본 삭제

2-2. 병합 규칙

설정 종류예drop-in에 쓰면
단일 값Nice=, User=, Type=덮어쓴다
목록 값ExecStart=, ExecStartPre=, Environment=, After=추가된다
목록 초기화ExecStart= (빈 값)기존 목록을 비운다

Type=simple 서비스는 ExecStart를 하나만 가질 수 있다. 그래서 ExecStart를 바꾸려면 빈 값으로 먼저 비우고 새 값을 적어야 한다.


3. 동작 원리

drop-in 병합 구조 — 원본은 그대로, 차이만 덧붙인다

systemctl daemon-reload
  → labapp.service 읽기
  → labapp.service.d/exec.conf 읽기      (알파벳 순: exec < override)
  → labapp.service.d/override.conf 읽기
  → 병합 결과 검증 → 오류면 "bad unit file setting" 으로 로드 거부
systemctl restart labapp → 병합된 최종 설정으로 실행

systemctl edit은 편집기를 열어 override.conf를 만들고 저장 시 daemon-reload까지 해 준다. 스크립트·자동화에서는 이번 실습처럼 파일을 직접 작성하고 daemon-reload 하는 방식이 재현성이 높다.


4. 명령어 실습

# 1) 현재 값 확인 → drop-in 작성 → 적용
systemctl show labapp -p Environment,Nice,LimitNOFILE,MemoryMax --no-pager
mkdir -p /etc/systemd/system/labapp.service.d
cat > /etc/systemd/system/labapp.service.d/override.conf <<'EOF'
[Service]
Environment=LAB_PORT=8082
Nice=5
LimitNOFILE=4096
MemoryMax=200M
EOF
systemctl daemon-reload; systemctl restart labapp
systemctl cat labapp --no-pager | tail -8
systemctl show labapp -p Environment,Nice,LimitNOFILE,MemoryMax,DropInPaths --no-pager
ss -tlnp | grep python3

# 2) ExecStart 교체: 잘못된 방법 → 올바른 방법
printf '[Service]\nExecStart=/usr/bin/python3 -u /opt/labapp/app.py\n' > /etc/systemd/system/labapp.service.d/exec.conf
systemctl daemon-reload; systemctl restart labapp
journalctl -u labapp --no-pager -o cat | grep -i 'more than one' | tail -1
printf '[Service]\nExecStart=\nExecStart=/usr/bin/python3 -u /opt/labapp/app.py\n' > /etc/systemd/system/labapp.service.d/exec.conf
systemctl daemon-reload; systemctl restart labapp
systemd-delta --type=extended --no-pager

# 3) (Ubuntu) 배포판이 기본으로 넣어 둔 drop-in
systemd-delta --no-pager | head -8

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 원본을 건드리지 않고 설정 바꾸기

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — ExecStart 는 먼저 비워야 바꿀 수 있다

실제 실행 결과 — Ubuntu 24.04.5 · root@ubuntu-lab — 배포판 기본 drop-in 찾기

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

[root@rocky9-lab ~]# systemctl show labapp -p Environment,Nice,LimitNOFILE,MemoryMax --no-pager
MemoryMax=infinity
Environment=LAB_PORT=8081
LimitNOFILE=524288
Nice=0
[root@rocky9-lab ~]# mkdir -p /etc/systemd/system/labapp.service.d
[root@rocky9-lab ~]# cat > /etc/systemd/system/labapp.service.d/override.conf <<'EOF'
> [Service]
> Environment=LAB_PORT=8082
> Nice=5
> LimitNOFILE=4096
> MemoryMax=200M
> EOF
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl restart labapp; sleep 1
[root@rocky9-lab ~]# systemctl cat labapp --no-pager | tail -8
WantedBy=multi-user.target

# /etc/systemd/system/labapp.service.d/override.conf
[Service]
Environment=LAB_PORT=8082
Nice=5
LimitNOFILE=4096
MemoryMax=200M
[root@rocky9-lab ~]# systemctl show labapp -p Environment,Nice,LimitNOFILE,MemoryMax,DropInPaths --no-pager
MemoryMax=209715200
Environment=LAB_PORT=8082
LimitNOFILE=4096
Nice=5
DropInPaths=/etc/systemd/system/labapp.service.d/override.conf
[root@rocky9-lab ~]# ss -tlnp | grep python3
LISTEN   0        5              127.0.0.1:8082           0.0.0.0:*       users:(("python3",pid=3204,fd=3))
[root@rocky9-lab ~]# printf '[Service]\nExecStart=/usr/bin/python3 -u /opt/labapp/app.py\n' > /etc/systemd/system/labapp.service.d/exec.conf
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl restart labapp; systemctl status labapp --no-pager | sed -n '3p'
Failed to restart labapp.service: Unit labapp.service has a bad unit file setting.
See system logs and 'systemctl status labapp.service' for details.
Warning: The unit file, source configuration file or drop-ins of labapp.service changed on disk. Run 'systemctl daemon-reload' to reload units.
    Drop-In: /etc/systemd/system/labapp.service.d
[root@rocky9-lab ~]# journalctl -u labapp --no-pager -o cat | grep -i 'more than one' | tail -1
labapp.service: Service has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.
[root@rocky9-lab ~]# printf '[Service]\nExecStart=\nExecStart=/usr/bin/python3 -u /opt/labapp/app.py\n' > /etc/systemd/system/labapp.service.d/exec.conf
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl restart labapp; systemctl status labapp --no-pager | sed -n '3p'
    Drop-In: /etc/systemd/system/labapp.service.d
[root@rocky9-lab ~]# systemd-delta --type=extended --no-pager
[EXTENDED]   /etc/systemd/system/labapp.service → /etc/systemd/system/labapp.service.d/exec.conf
[EXTENDED]   /etc/systemd/system/labapp.service → /etc/systemd/system/labapp.service.d/override.conf
[EXTENDED]   /usr/lib/systemd/system/systemd-hostnamed.service → /usr/lib/systemd/system/systemd-hostnamed.service.d/disable-privatedevices.conf
[EXTENDED]   /usr/lib/systemd/system/user@.service → /usr/lib/systemd/system/user@.service.d/10-login-barrier.conf

4 overridden configuration files found.
root@ubuntu-lab:~# systemd-delta --no-pager | head -8
[EXTENDED]   /usr/lib/systemd/system/rc-local.service → /usr/lib/systemd/system/rc-local.service.d/debian.conf
[EXTENDED]   /usr/lib/systemd/system/systemd-journald.service → /usr/lib/systemd/system/systemd-journald.service.d/nice.conf
[EXTENDED]   /usr/lib/systemd/system/systemd-localed.service → /usr/lib/systemd/system/systemd-localed.service.d/x11-keyboard.conf
[EXTENDED]   /usr/lib/systemd/system/systemd-logind.service → /usr/lib/systemd/system/systemd-logind.service.d/dbus.conf
[EXTENDED]   /usr/lib/systemd/system/user@.service → /usr/lib/systemd/system/user@.service.d/10-login-barrier.conf
[EXTENDED]   /usr/lib/systemd/system/user@.service → /usr/lib/systemd/system/user@.service.d/timeout.conf

6 overridden configuration files found.

6. 결과 해석

관찰의미
변경 전 LAB_PORT=8081, Nice=0, LimitNOFILE=524288, MemoryMax=infinity25편에서 만든 원본 설정과 systemd 기본값
systemctl cat 끝에 # /etc/.../override.confdrop-in이 원본 아래에 이어서 표시된다. 어떤 파일에서 온 설정인지 한눈에 보인다
변경 후 LAB_PORT=8082, Nice=5, LimitNOFILE=4096, MemoryMax=209715200네 값이 모두 바뀌었다. 200M = 209715200 바이트
DropInPaths=.../override.conf적용된 drop-in 목록. 점검 스크립트에서 유용하다
ss에서 python3가 127.0.0.1:8082원본을 수정하지 않고 앱 포트가 바뀌었다
빈 값 없이 ExecStart 추가 → Failed to restart ...: bad unit file settingExecStart가 두 개로 누적 되어 unit 로드가 거부되었다
로그 Service has more than one ExecStart= setting ... Refusing.원인이 정확히 기록된다
ExecStart= 빈 값 추가 후 정상 재시작목록을 초기화한 뒤 새 값으로 교체
systemd-delta --type=extended → labapp에 exec.conf, override.conf원본을 확장한 모든 drop-in 을 한 번에 보여 준다
Rocky 기본 drop-in: systemd-hostnamed ... disable-privatedevices.conf배포판도 drop-in으로 설정을 조정한다
Ubuntu systemd-journald.service.d/nice.conf09편에서 본 journald의 Nice=-1이 바로 이 drop-in 에서 왔다

7. 보안 관점

주제내용
보이지 않는 변조원본 unit 파일은 그대로이고 패키지 무결성 검사(rpm -V)도 통과한다. 그러나 sshd.service.d/x.conf에 ExecStartPre=/tmp/.x를 넣으면 sshd가 시작될 때마다 악성 코드가 실행 된다
ExecStart 가로채기ExecStart= 초기화 후 다른 바이너리를 지정하면, 서비스 이름·상태는 정상인데 실제로는 다른 프로그램 이 돈다
보안 설정 해제drop-in으로 NoNewPrivileges=no, ProtectSystem=no 등을 넣어 샌드박스를 무력화할 수 있다 (38편)
정상 운영반대로 보안 강화(샌드박싱, 자원 제한)는 drop-in으로 적용하면 패키지 업데이트에도 유지된다

8. 보안관제 관점

[점검]  systemd-delta --type=extended,overridden --no-pager       ← 원본을 바꾼 모든 설정
        find /etc/systemd/system /run/systemd/system -path '*.d/*.conf' -newermt '-7 days'
        grep -rhE '^(ExecStart|ExecStartPre|ExecStartPost|ExecStop)=' /etc/systemd/system/*.d/
     ↓
[판단]  패키지 기본 drop-in 과 관리자 변경 기록(변경 관리 티켓)에 없는 drop-in
        Exec* 가 /tmp · /dev/shm · 홈 디렉터리 · 인코딩된 명령을 가리키는가
     ↓
[Response]  해당 drop-in 격리 → daemon-reload → 서비스 재시작 → 생성 시각 기준 조사
명령용도
systemd-delta원본 대비 변경(extended, overridden, masked) 전체
systemctl show UNIT -p DropInPaths특정 서비스의 drop-in 목록
systemctl cat UNIT병합 결과를 파일 단위로

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

실수결과예방
/usr/lib/... 원본 직접 수정업데이트 시 소실drop-in 사용
ExecStart를 빈 값 없이 추가"more than one ExecStart" 로드 실패ExecStart= 먼저
drop-in에 섹션 헤더([Service]) 누락설정 무시섹션 헤더 필수
파일 확장자를 .conf가 아닌 것으로읽히지 않음*.conf만 적용된다
daemon-reload 없이 restart이전 설정으로 실행 (경고 표시)작성 → daemon-reload → restart

10. 실습 체크리스트

[ ] NAME.service.d/override.conf 로 설정 4개를 변경했다
[ ] systemctl cat 으로 원본과 drop-in 이 함께 표시되는 것을 확인했다
[ ] systemctl show 로 최종 적용 값을 확인했다
[ ] ExecStart 를 빈 값 없이 추가해 로드 실패를 재현했다
[ ] ExecStart= 초기화 후 교체에 성공했다
[ ] systemd-delta 로 전체 drop-in 을 점검했다

11. 핵심 정리

  • drop-in은 원본 unit을 수정하지 않고 NAME.service.d/*.conf로 차이만 덧붙이는 방법이다.
  • 단일 값은 덮어쓰고, 목록 값(ExecStart, Environment 등)은 누적된다. 교체하려면 빈 값으로 먼저 초기화한다.
  • systemctl cat, systemctl show -p DropInPaths, systemd-delta로 적용 결과를 확인한다.
  • 패키지 무결성 검사를 통과하는 drop-in 변조 는 지속성·방어 회피 수단이 될 수 있어 점검 대상이다.

12. 다음 편 예고

다음 글 「29. Restart 정책과 서비스 자동 복구」 에서는 Restart=always와 on-failure의 차이, 그리고 계속 실패하는 서비스가 StartLimit에 걸려 재시작을 포기 하는 과정을 실험한다. 공격자가 자동 재시작을 지속성에 악용하는 방식도 함께 본다.


참고 자료


시리즈 이동

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

0개의 댓글