systemd 서비스 하드닝은 유닛 파일에 샌드박싱 지시어를 추가해서, 서비스가 침해당했을 때 공격자가 할 수 있는 일을 미리 좁혀두는 작업이다. 파일시스템을 읽기 전용으로 만들고, 커널 기능을 뺏고, 불필요한 시스템 콜을 차단하는 식이다. 놀라운 건 이 모든 걸 컨테이너나 별도 도구 없이 ExecStart 주변에 몇 줄 추가하는 것만으로 할 수 있다는 점이다.
내가 이걸 진지하게 파기 시작한 건 systemd-analyze security라는 명령을 우연히 돌려보고 나서였다. 자체 제작한 서비스가 "UNSAFE"라는 빨간 글씨와 함께 노출 점수 9.6을 받은 걸 보고 좀 부끄러웠다. 오늘은 그 점수를 어떻게 1점대까지 내렸는지 적는다.
먼저 현재 상태를 측정한다
하드닝은 감으로 하면 안 된다. 측정 가능한 지표부터 확보한다. systemd에는 서비스별로 노출 점수를 매겨주는 내장 도구가 있다.
systemd-analyze security myapp.service
실행하면 각 지시어별로 exposure(노출도)를 매기고, 마지막에 0(안전)부터 10(위험)까지의 종합 점수와 등급을 보여준다. 처음 돌리면 대부분의 커스텀 서비스가 8~10 사이에 몰려 있을 것이다. 기본값이 "아무 제한 없음"이기 때문이다. 이 명령이 좋은 건, 지시어를 하나 추가할 때마다 점수가 얼마나 떨어지는지 즉시 피드백을 준다는 점이다.
가장 효과 큰 지시어 몇 개
지시어가 수십 개지만, 처음부터 다 넣을 필요는 없다. 투자 대비 효과가 큰 것부터 넣는다. 아래는 내가 거의 모든 서비스에 기본으로 넣는 조합이다.
[Service]
# 파일시스템 보호
ProtectSystem=strict # /usr, /boot 등 전부 읽기 전용
ProtectHome=true # /home, /root 접근 차단
ReadWritePaths=/var/lib/myapp # 쓰기 필요한 곳만 예외
PrivateTmp=true # 서비스 전용 /tmp 격리
# 권한 상승 차단
NoNewPrivileges=true # setuid로도 권한 못 올림
RestrictSUIDSGID=true
# 커널 표면 축소
ProtectKernelTunables=true # /proc/sys 쓰기 금지
ProtectKernelModules=true # 모듈 로드 금지
ProtectControlGroups=true
RestrictNamespaces=true
# 네트워크·시스템콜
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
ProtectSystem=strict는 체감 효과가 가장 크다. 이걸 켜면 서비스는 파일시스템 전체를 읽기 전용으로 보게 되고, 쓰기가 필요한 경로만 ReadWritePaths로 명시적으로 열어준다. 만약 서비스가 침해돼도 시스템 바이너리를 덮어쓰거나 백도어를 심는 게 막힌다.
하드닝의 사고방식은 "무엇을 막을까"가 아니라 "무엇을 허용할까"다. 기본을 전부 닫고 필요한 것만 여는 순간, 공격면은 극적으로 줄어든다.
SystemCallFilter, 조금 더 설명
SystemCallFilter=@system-service는 일반적인 서비스가 쓰는 시스템 콜 묶음만 허용하고 나머지는 차단한다. 뒤에 붙인 ~@privileged @resources는 그중에서도 권한·리소스 조작 계열을 추가로 뺀다. 앞의 ~가 "제외"를 뜻한다.
주의할 점 하나. 이 필터를 너무 조이면 앱이 특정 시스템 콜을 못 써서 이상하게 죽는다. 그럴 땐 journalctl에서 SECCOMP 키워드로 로그를 보면 어떤 콜이 막혔는지 나온다. 나는 처음에 @resources를 뺐다가 메모리를 많이 쓰는 워커가 죽어서, 한참 헤맨 적이 있다. 로그를 봤으면 5분에 끝날 일이었다.
지시어별 효과를 표로
| 지시어 | 막는 것 | 주의 |
|---|---|---|
| ProtectSystem=strict | 시스템 파일 변조 | ReadWritePaths로 쓰기 경로 열기 |
| NoNewPrivileges=true | 권한 상승 | sudo 호출하는 서비스는 깨짐 |
| PrivateTmp=true | /tmp 통한 공격 | 거의 항상 안전하게 켜기 가능 |
| RestrictAddressFamilies | 불필요한 소켓 | UNIX 소켓 쓰면 AF_UNIX 포함 |
override로 안전하게 적용하기
패키지가 설치한 유닛 파일을 직접 고치면 업데이트 때 덮어써진다. 그래서 나는 항상 drop-in override를 쓴다.
sudo systemctl edit myapp.service
이러면 /etc/systemd/system/myapp.service.d/override.conf가 열리고, 여기 적은 내용이 원본 위에 얹힌다. 하드닝 지시어를 여기 넣고 daemon-reload 후 재시작하면 된다. 한 서비스씩 적용하고, 매번 systemd-analyze security로 점수가 내려가는지, 앱이 여전히 잘 도는지 확인하는 게 안전하다. 개인적으로는 한 번에 다 넣고 뭐가 깨졌는지 몰라 헤매느니, 한 줄씩 넣고 확인하는 게 훨씬 빠르다고 느꼈다.
자주 묻는 질문
systemd 하드닝이 컨테이너 격리를 대체하나요?
완전히 대체하진 않지만 상당 부분 겹칩니다. ProtectSystem, PrivateTmp, RestrictNamespaces 같은 지시어는 컨테이너 없이도 파일시스템·네임스페이스 격리를 제공합니다. 컨테이너를 쓸 수 없는 베어메탈 서비스나 시스템 데몬에 특히 유용합니다.
지시어를 넣었더니 서비스가 안 뜹니다.
대부분 쓰기 경로가 막혔거나 시스템 콜이 차단된 경우입니다. journalctl 로그에서 "Read-only file system" 또는 "SECCOMP"를 검색하세요. 전자는 ReadWritePaths에 해당 경로를 추가하고, 후자는 SystemCallFilter를 완화하면 해결됩니다.
어느 지시어부터 넣는 게 좋나요?
부작용이 거의 없으면서 효과가 큰 것부터 시작하세요. PrivateTmp, NoNewPrivileges, ProtectHome은 대부분의 서비스에서 안전하게 켤 수 있습니다. ProtectSystem=strict와 SystemCallFilter는 앱 동작을 확인하며 신중하게 적용하는 게 좋습니다.
노출 점수를 어디까지 낮춰야 하나요?
0에 집착할 필요는 없습니다. 인터넷에 노출된 서비스라면 3점 이하(OK 등급)를 목표로 잡되, 앱이 정상 동작하는 선에서 멈추면 됩니다. 점수를 낮추려다 기능이 깨지면 본말전도입니다.

댓글 0