
서비스 · 프로세스 관리 28 / 50 · Part 3. systemd와 서비스
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
nginx의 파일 한도를 올리거나 서비스에 환경변수 하나를 추가하려고 /usr/lib/systemd/system/nginx.service를 직접 고쳤다. 몇 주 뒤 패키지 업데이트가 설치되자 변경 사항이 사라지고 장애가 재발했다. 패키지가 원본 파일을 덮어썼기 때문이다.
systemd는 이 문제를 drop-in 으로 해결한다. 원본은 그대로 두고, 서비스명.service.d/ 디렉터리에 바꿀 부분만 담은 .conf 파일 을 두면 systemd가 합쳐서 적용한다. 이번 글에서는 drop-in으로 설정을 바꾸고, 특히 자주 실수하는 ExecStart 교체 규칙 을 실패 로그로 확인한다.
| 항목 | 내용 |
|---|---|
| 위치 | /etc/systemd/system/NAME.service.d/*.conf (관리자), /usr/lib/systemd/system/NAME.service.d/ (패키지), /run/... |
| 적용 순서 | 메인 unit → drop-in 파일명 알파벳 순 (같은 이름이면 /etc 우선) |
| 공통 drop-in | service.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·복사본 삭제 |
| 설정 종류 | 예 | drop-in에 쓰면 |
|---|---|---|
| 단일 값 | Nice=, User=, Type= | 덮어쓴다 |
| 목록 값 | ExecStart=, ExecStartPre=, Environment=, After= | 추가된다 |
| 목록 초기화 | ExecStart= (빈 값) | 기존 목록을 비운다 |
Type=simple 서비스는 ExecStart를 하나만 가질 수 있다. 그래서 ExecStart를 바꾸려면 빈 값으로 먼저 비우고 새 값을 적어야 한다.

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 하는 방식이 재현성이 높다.
# 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



텍스트 원본(실제 출력):
[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.
| 관찰 | 의미 |
|---|---|
변경 전 LAB_PORT=8081, Nice=0, LimitNOFILE=524288, MemoryMax=infinity | 25편에서 만든 원본 설정과 systemd 기본값 |
systemctl cat 끝에 # /etc/.../override.conf | drop-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 setting | ExecStart가 두 개로 누적 되어 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.conf | 09편에서 본 journald의 Nice=-1이 바로 이 drop-in 에서 왔다 |
| 주제 | 내용 |
|---|---|
| 보이지 않는 변조 | 원본 unit 파일은 그대로이고 패키지 무결성 검사(rpm -V)도 통과한다. 그러나 sshd.service.d/x.conf에 ExecStartPre=/tmp/.x를 넣으면 sshd가 시작될 때마다 악성 코드가 실행 된다 |
| ExecStart 가로채기 | ExecStart= 초기화 후 다른 바이너리를 지정하면, 서비스 이름·상태는 정상인데 실제로는 다른 프로그램 이 돈다 |
| 보안 설정 해제 | drop-in으로 NoNewPrivileges=no, ProtectSystem=no 등을 넣어 샌드박스를 무력화할 수 있다 (38편) |
| 정상 운영 | 반대로 보안 강화(샌드박싱, 자원 제한)는 drop-in으로 적용하면 패키지 업데이트에도 유지된다 |
[점검] 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 | 병합 결과를 파일 단위로 |
| 실수 | 결과 | 예방 |
|---|---|---|
/usr/lib/... 원본 직접 수정 | 업데이트 시 소실 | drop-in 사용 |
| ExecStart를 빈 값 없이 추가 | "more than one ExecStart" 로드 실패 | ExecStart= 먼저 |
drop-in에 섹션 헤더([Service]) 누락 | 설정 무시 | 섹션 헤더 필수 |
파일 확장자를 .conf가 아닌 것으로 | 읽히지 않음 | *.conf만 적용된다 |
| daemon-reload 없이 restart | 이전 설정으로 실행 (경고 표시) | 작성 → daemon-reload → restart |
[ ] NAME.service.d/override.conf 로 설정 4개를 변경했다
[ ] systemctl cat 으로 원본과 drop-in 이 함께 표시되는 것을 확인했다
[ ] systemctl show 로 최종 적용 값을 확인했다
[ ] ExecStart 를 빈 값 없이 추가해 로드 실패를 재현했다
[ ] ExecStart= 초기화 후 교체에 성공했다
[ ] systemd-delta 로 전체 drop-in 을 점검했다
NAME.service.d/*.conf로 차이만 덧붙이는 방법이다.systemctl cat, systemctl show -p DropInPaths, systemd-delta로 적용 결과를 확인한다.다음 글 「29. Restart 정책과 서비스 자동 복구」 에서는 Restart=always와 on-failure의 차이, 그리고 계속 실패하는 서비스가 StartLimit에 걸려 재시작을 포기 하는 과정을 실험한다. 공격자가 자동 재시작을 지속성에 악용하는 방식도 함께 본다.