
서비스 · 프로세스 관리 37 / 50 · Part 4. 로그·스케줄링·운영
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
16편에서 Ubuntu의 22번 포트를 sshd와 PID 1(systemd) 이 함께 쥐고 있는 것을 보았다. 22편에서는 그 이유가 ssh.socket이라는 unit이라는 것까지 확인했다. 이 구조를 소켓 활성화(socket activation) 라고 한다. systemd가 먼저 포트를 열어 두고, 연결이 들어오면 그때 서비스를 실행하거나 소켓을 넘겨준다.
이번 글에서는 연결이 올 때마다 짧은 응답 서비스를 실행하는 socket unit을 직접 만들어 동작을 확인하고, Ubuntu ssh.socket의 실제 구성을 읽는다. 관제에서 "포트의 주인이 systemd로 보이는" 상황을 해석하는 기준도 정리한다.
| 설정 | 의미 |
|---|---|
ListenStream= | TCP(또는 UNIX 스트림) 주소·포트 |
ListenDatagram= | UDP |
ListenFIFO= | 이름 있는 파이프 |
Accept= | yes: 연결마다 서비스 인스턴스 생성 / no(기본): 소켓을 서비스 하나에 전달 |
Service= | 활성화할 서비스 (기본: 같은 이름) |
BindIPv6Only=, FreeBind= | 바인딩 세부 옵션 |
MaxConnections= | 동시 연결 제한 (Accept=yes) |
| 구분 | Accept=yes | Accept=no |
|---|---|---|
| 서비스 unit | 템플릿 NAME@.service | 일반 NAME.service |
| 프로세스 | 연결마다 하나 (inetd 방식) | 서비스 하나가 모든 연결 처리 |
| 서비스가 받는 것 | 연결된 소켓 (stdin/stdout) | listen 소켓 |
| 예 | 간단한 내부 도구 | sshd, journald, dbus |
| 명령 | 용도 |
|---|---|
systemctl list-sockets | 소켓 unit과 포트, 활성화 대상 |
systemctl status X.socket | 상태, Listen 주소, Triggers |
systemctl show X.socket -p NAccepted,NConnections | 누적/현재 연결 수 |
systemctl show X.service -p TriggeredBy | 어떤 소켓이 이 서비스를 띄우는가 |

systemctl enable --now lab-echo.socket
→ PID 1 이 127.0.0.1:9999 에 bind · listen (서비스 프로세스는 아직 없음)
클라이언트 연결
→ PID 1 이 accept → lab-echo@<번호>-<로컬>-<원격>.service 인스턴스 생성
→ 연결 fd 를 표준입력·출력으로 연결해 ExecStart 실행
→ 한 줄 읽고 응답 → 종료 → 인스턴스 정리
서비스 입장에서는 표준입력으로 읽고 표준출력으로 쓰기만 하면 된다. 네트워크 코드가 없어도 네트워크 서비스가 되는 구조다.
# 1) (Rocky) socket unit + 템플릿 서비스
printf '[Unit]\nDescription=Lab echo socket\n[Socket]\nListenStream=127.0.0.1:9999\nAccept=yes\n[Install]\nWantedBy=sockets.target\n' \
> /etc/systemd/system/lab-echo.socket
printf '[Unit]\nDescription=Lab echo per-connection service\n[Service]\nExecStart=/bin/bash -c "read line; echo \\"hello $$line from PID $$$$ at $$(date +%%%%T)\\""\nStandardInput=socket\n' \
> /etc/systemd/system/lab-echo@.service
systemctl daemon-reload; systemctl enable --now lab-echo.socket
ss -tlnp 'sport = :9999' # 포트 주인 확인
systemctl list-units 'lab-echo*' --no-pager --no-legend
echo analyst | nc 127.0.0.1 9999; echo soc | nc 127.0.0.1 9999
systemctl show lab-echo.socket -p NAccepted,NConnections
journalctl -u 'lab-echo@*' --no-pager -o cat | grep -E 'Started|Deactivated' | tail -4
# 2) (Ubuntu) 실제 사례 ssh.socket
systemctl list-sockets --no-pager
systemctl status ssh.socket --no-pager | sed -n '1,6p'
systemctl show ssh.service -p TriggeredBy,ActiveState
ss -tlnp 'sport = :22'
unit 파일 안에서
$는 systemd의 환경변수 치환,%는 지정자다. 셸에 그대로 넘기려면$$,%%로 쓴다(35편과 같은 규칙). printf 단계의 이스케이프까지 겹쳐$$$$,%%%%가 되었다.


텍스트 원본(실제 출력):
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab echo socket\n[Socket]\nListenStream=127.0.0.1:9999\nAccept=yes\n[Install]\nWantedBy=sockets.target\n' > /etc/systemd/system/lab-echo.socket
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab echo per-connection service\n[Service]\nExecStart=/bin/bash -c "read line; echo \\"hello $$line from PID $$$$ at $$(date +%%%%T)\\""\nStandardInput=socket\n' > /etc/systemd/system/lab-echo@.service
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl enable --now lab-echo.socket
Created symlink /etc/systemd/system/sockets.target.wants/lab-echo.socket → /etc/systemd/system/lab-echo.socket.
[root@rocky9-lab ~]# ss -tlnp 'sport = :9999'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:9999 0.0.0.0:* users:(("systemd",pid=1,fd=38))
[root@rocky9-lab ~]# systemctl list-units 'lab-echo*' --no-pager --no-legend
lab-echo.socket loaded active listening Lab echo socket
[root@rocky9-lab ~]# echo analyst | nc 127.0.0.1 9999; echo soc | nc 127.0.0.1 9999
hello analyst from PID 5891 at 12:32:08
hello soc from PID 5917 at 12:32:08
[root@rocky9-lab ~]# systemctl show lab-echo.socket -p NAccepted,NConnections
NConnections=0
NAccepted=2
[root@rocky9-lab ~]# journalctl -u 'lab-echo@*' --no-pager -o cat | grep -E 'Started|Deactivated' | tail -4
Started Lab echo per-connection service (127.0.0.1:49452).
lab-echo@0-127.0.0.1:9999-127.0.0.1:49452.service: Deactivated successfully.
Started Lab echo per-connection service (127.0.0.1:49468).
lab-echo@1-127.0.0.1:9999-127.0.0.1:49468.service: Deactivated successfully.
root@ubuntu-lab:~# systemctl list-sockets --no-pager
LISTEN UNIT ACTIVATES
0.0.0.0:22 ssh.socket ssh.service
/run/dbus/system_bus_socket dbus.socket dbus.service
/run/initctl systemd-initctl.socket systemd-initctl.service
/run/systemd/io.systemd.sysext systemd-sysext.socket -
/run/systemd/journal/dev-log systemd-journald-dev-log.socket systemd-journald.service
/run/systemd/journal/socket systemd-journald.socket systemd-journald.service
/run/systemd/journal/stdout systemd-journald.socket systemd-journald.service
7 sockets listed.
Pass --all to see loaded but inactive sockets, too.
root@ubuntu-lab:~# systemctl status ssh.socket --no-pager | sed -n '1,6p'
● ssh.socket - OpenBSD Secure Shell server socket
Loaded: loaded (/usr/lib/systemd/system/ssh.socket; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-24 11:48:28 UTC; 43min ago
Triggers: ● ssh.service
Listen: 0.0.0.0:22 (Stream)
Tasks: 0 (limit: 9617)
root@ubuntu-lab:~# systemctl show ssh.service -p TriggeredBy,ActiveState
TriggeredBy=ssh.socket
ActiveState=active
root@ubuntu-lab:~# ss -tlnp 'sport = :22'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=95,fd=3),("systemd",pid=1,fd=75))
| 관찰 | 의미 |
|---|---|
LISTEN 127.0.0.1:9999 users:(("systemd",pid=1,fd=38)) | 포트를 연 것은 PID 1 이다. 서비스 프로세스는 아직 없다 |
lab-echo.socket loaded active listening | 소켓 unit만 active. 서비스 인스턴스는 목록에 없다 |
hello analyst from PID 5891, hello soc from PID 5917 | 연결마다 다른 PID 의 프로세스가 응답했다 (Accept=yes) |
NAccepted=2, NConnections=0 | 누적 2회 연결을 받았고, 지금 열린 연결은 없다 |
Started ... (127.0.0.1:49452) → lab-echo@0-127.0.0.1:9999-127.0.0.1:49452.service: Deactivated | 인스턴스 이름에 접속 순번·로컬·원격 주소 가 들어간다. 로그만으로 누가 접속했는지 알 수 있다 |
Ubuntu list-sockets: 0.0.0.0:22 ssh.socket → ssh.service | 22번 포트는 ssh.socket이 열고 ssh.service를 활성화한다 |
| journald·dbus도 소켓 unit | 시스템 기본 서비스도 소켓 활성화로 병렬 부팅한다 |
ssh.service TriggeredBy=ssh.socket | 서비스 쪽에서 활성화 주체를 확인할 수 있다 |
ss 22번 포트에 sshd(95)와 systemd(1) | Accept=no라 listen 소켓을 공유 한다. 16편에서 본 구조의 정체다 |
| 주제 | 내용 |
|---|---|
| 포트 주인 해석 | ss -p에 systemd만 보이면 프로세스 목록으로는 원인을 찾을 수 없다. socket unit과 활성화 대상 서비스 를 확인해야 한다 |
| 숨은 서비스 | 소켓 활성화 서비스는 평소에는 프로세스가 없어 ps 점검에 드러나지 않는다. 포트 점검(16편)과 list-sockets를 함께 본다 |
| 노출 범위 | ListenStream=9999처럼 주소 없이 쓰면 모든 인터페이스 에 열린다. 내부용은 127.0.0.1:포트로 제한한다 |
| 자원 보호 | Accept=yes 서비스는 연결마다 프로세스를 만든다. MaxConnections=, 서비스 쪽 TasksMax=로 과부하를 막는다 |
| 권한 분리 | 낮은 포트는 PID 1이 열고 서비스는 fd만 받으므로, 서비스는 권한 없는 계정으로 돌릴 수 있다 (30편) |
[Detection] 외부 스캔 결과: 서버에 예상하지 않은 TCP 포트 open
↓
[서버 확인] ss -tlnp 'sport = :<포트>' → users:(("systemd",pid=1,...))
↓
[소켓 unit] systemctl list-sockets --all | grep <포트>
systemctl cat <이름>.socket <이름>@.service (또는 .service)
↓
[판단] 패키지 소유 여부(rpm -qf / dpkg -S), 생성 시각, 활성화 서비스의 ExecStart
↓
[기록] journalctl -u '<이름>@*' → 접속 순번·원격 주소가 인스턴스 이름에 남음
| 명령 | 목적 |
|---|---|
systemctl list-sockets --all | 모든 소켓 unit과 주소 |
ss -tlnpe | 포트 + 소속 cgroup(어느 .socket인지) |
systemctl show X.socket -p NAccepted | 누적 접속 수 |
| 실수 | 결과 | 예방 |
|---|---|---|
| 서비스만 stop | 소켓이 남아 있어 다음 연결에 다시 시작 | socket unit도 stop/disable |
| Accept=yes인데 일반 서비스 이름 사용 | 인스턴스 생성 실패 | NAME@.service 템플릿 |
| ssh.socket의 포트를 sshd_config만 바꿈 | 포트가 안 바뀜 (Ubuntu 24.04) | ssh.socket의 ListenStream도 수정(또는 drop-in) |
| 주소 없이 포트만 지정 | 모든 인터페이스 노출 | 바인딩 주소 명시 |
ExecStart의 $, % 이스케이프 누락 | 빈 값·지정자로 치환 | $$, %% |
[ ] socket unit 과 템플릿 서비스를 작성했다
[ ] 연결 전에는 포트 주인이 PID 1 이고 서비스 프로세스가 없는 것을 확인했다
[ ] 연결마다 다른 PID 의 인스턴스가 실행되는 것을 확인했다
[ ] NAccepted 와 인스턴스 이름의 원격 주소를 확인했다
[ ] Ubuntu ssh.socket → ssh.service 활성화 구조를 확인했다
[ ] 포트 주인이 systemd 일 때 추적하는 순서를 설명할 수 있다
Accept=yes는 연결마다 인스턴스, Accept=no는 서비스 하나가 listen 소켓을 받는다.list-sockets로 socket unit과 활성화 서비스를 찾는다.ssh.socket으로 동작해 포트 변경도 socket unit에서 해야 한다.다음 글 「38. systemd 서비스 샌드박싱」 에서는 systemd-analyze security로 서비스의 노출 점수 를 측정하고, ProtectSystem, PrivateTmp, NoNewPrivileges 같은 샌드박스 옵션을 drop-in으로 적용해 점수가 어떻게 바뀌는지 확인한다.