
서비스 · 프로세스 관리 31 / 50 · Part 4. 로그·스케줄링·운영
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
Part 4 「로그·스케줄링·운영」을 시작한다. 지금까지 거의 모든 편에서 journalctl -u 서비스로 로그를 확인했다. systemd 환경에서 서비스 출력, 인증 로그, sudo 기록, 커널 메시지는 모두 journald 에 모인다. 보안관제 요원에게 journalctl은 grep만큼 기본적인 도구다.
이번 글에서는 unit·시간·우선순위·패턴·필드 로 로그를 거르는 방법을 체계적으로 정리하고, 실제로 SSH 로그인 실패와 sudo 사용 이벤트를 만들어 journal에서 보안 이벤트를 뽑아내는 실습을 한다.
「리눅스 시스템 기초 48편」에서 journalctl 기본을, 「49편」에서 SSH Brute Force 로그 분석을 다뤘다. 이번 글은 journald의 구조화 필드 활용에 집중한다.
| 항목 | 전통 syslog 파일 | journald |
|---|---|---|
| 저장 | 텍스트 한 줄 | 바이너리 + 필드(key=value) |
| 메타데이터 | 시간·호스트·프로그램 이름 정도 | PID, UID, 실행 파일, 명령줄, unit, 부팅 ID 등 |
| 신뢰성 | 프로그램이 쓴 그대로 (위조 가능) | _로 시작하는 필드는 journald가 커널 정보로 기록 |
| 검색 | grep | 필드 인덱스 검색 (_UID=1000) + 정규식(-g) |
| 옵션 | 예 | 의미 |
|---|---|---|
-u | -u sshd | 서비스(unit) |
-t | -t CROND | SYSLOG_IDENTIFIER (프로그램 태그) |
--since / --until | --since "-2min", "2026-09-24 12:00" | 시간 범위 |
-b | -b, -b -1 | 이번/이전 부팅 |
-p | -p warning, -p err | 우선순위 이상 |
-g | -g 'Invalid user' | MESSAGE 정규식 |
-k | 커널 로그 | |
| 필드 매칭 | _COMM=sudo, _UID=0 | 특정 필드 값 |
-F 필드 | -F _SYSTEMD_UNIT | 필드에 기록된 값 목록 |
-n / -f | -n 50, -f | 마지막 N줄 / 실시간 추적 |
-o)| 형식 | 용도 |
|---|---|
short (기본) / short-iso / short-precise | 사람이 읽기, ISO 시간, 마이크로초 |
cat | 메시지만 (파싱·집계용) |
verbose | 모든 필드 |
json / json-pretty | SIEM 전송·스크립트 처리 |

sudo 가 syslog("... COMMAND=/bin/cat /etc/shadow") 호출
→ /dev/log 소켓 → journald
→ journald: 보낸 프로세스의 PID·UID·실행 파일·cgroup 을 커널에서 확인해 _ 필드로 부착
→ 저장 (MESSAGE + PRIORITY + SYSLOG_IDENTIFIER + _PID + _UID + _EXE + _SYSTEMD_UNIT ...)
journalctl _COMM=sudo → 인덱스로 즉시 검색
메시지 내용은 프로그램이 마음대로 쓸 수 있지만, _UID, _EXE, _SYSTEMD_UNIT 같은 밑줄 필드는 journald가 커널에서 얻은 값 이다. "누가 이 로그를 남겼나"를 신뢰할 수 있는 근거가 된다.
# 1) 보안 이벤트 만들기: 존재하지 않는 계정으로 SSH 3회, analyst 의 sudo 2회
for u in admin oracle test; do ssh -o BatchMode=yes $u@localhost true 2>/dev/null; done
su - analyst -c 'sudo -n cat /etc/shadow > /dev/null; sudo -n id -u'
# 2) unit · 시간 · 패턴 · 우선순위
journalctl -u sshd --since "-2min" --no-pager -o short-iso | tail -6
journalctl -u sshd --since "-2min" --no-pager -g 'Invalid user' -o cat
journalctl _COMM=sudo --since "-2min" --no-pager -o cat
journalctl -p warning --since "-10min" --no-pager -o short-iso | tail -4
# 3) 필드 보기와 활용
journalctl _COMM=sudo -n 1 -o verbose --no-pager | grep -E '^\s+(_PID|_UID|_COMM|_EXE|_CMDLINE|_SYSTEMD_UNIT|SYSLOG_IDENTIFIER|PRIORITY|MESSAGE)='
journalctl -u sshd -g 'Invalid user' -o json --no-pager | tail -1 | python3 -c '...필드 추출...'
journalctl -F _SYSTEMD_UNIT --no-pager | sort | head -8
journalctl -u sshd --since "-2min" -g 'Invalid user' -o cat | awk '{print $3}' | sort | uniq -c
실습 계정 analyst는 lab 환경에서 비밀번호 없는 sudo가 허용되어 있다. 그래서
/etc/shadow읽기가 성공 했고, 그 기록이 남았다. 실제 서버라면 이런 권한 자체가 점검 대상이다.



텍스트 원본(실제 출력):
[root@rocky9-lab ~]# for u in admin oracle test; do ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o LogLevel=ERROR $u@localhost true 2>/dev/null; done; echo "잘못된 계정 SSH 시도 3회"
잘못된 계정 SSH 시도 3회
[root@rocky9-lab ~]# su - analyst -c 'sudo -n cat /etc/shadow > /dev/null; sudo -n id -u'
0
[root@rocky9-lab ~]# journalctl -u sshd --since "-2min" --no-pager -o short-iso | tail -6
2026-09-24T12:22:57+0000 rocky9-lab sshd-session[4567]: Connection closed by invalid user oracle 127.0.0.1 port 59346 [preauth]
2026-09-24T12:22:57+0000 rocky9-lab sshd-session[4570]: Invalid user test from 127.0.0.1 port 59360
2026-09-24T12:22:57+0000 rocky9-lab sshd-session[4570]: Connection closed by invalid user test 127.0.0.1 port 59360 [preauth]
2026-09-24T12:22:57+0000 rocky9-lab sshd-session[4525]: pam_unix(sshd:session): session closed for user root
2026-09-24T12:22:57+0000 rocky9-lab sshd-session[4610]: Accepted publickey for root from 172.17.0.1 port 44308 ssh2: ED25519 SHA256:Svd4EDEkiIH4de8x0M9DZ6BinOc0V/ysEmvVqU0Niug
2026-09-24T12:22:57+0000 rocky9-lab sshd-session[4610]: pam_unix(sshd:session): session opened for user root(uid=0) by root(uid=0)
[root@rocky9-lab ~]# journalctl -u sshd --since "-2min" --no-pager -g 'Invalid user' -o cat
Invalid user admin from 127.0.0.1 port 42946
Invalid user oracle from 127.0.0.1 port 42958
Invalid user test from 127.0.0.1 port 42968
Invalid user admin from 127.0.0.1 port 59330
Invalid user oracle from 127.0.0.1 port 59346
Invalid user test from 127.0.0.1 port 59360
[root@rocky9-lab ~]# journalctl _COMM=sudo --since "-2min" --no-pager -o cat
analyst : PWD=/home/analyst ; USER=root ; COMMAND=/bin/cat /etc/shadow
pam_unix(sudo:session): session opened for user root(uid=0) by root(uid=1000)
pam_unix(sudo:session): session closed for user root
analyst : PWD=/home/analyst ; USER=root ; COMMAND=/bin/id -u
pam_unix(sudo:session): session opened for user root(uid=0) by root(uid=1000)
pam_unix(sudo:session): session closed for user root
analyst : PWD=/home/analyst ; USER=root ; COMMAND=/bin/cat /etc/shadow
pam_unix(sudo:session): session opened for user root(uid=0) by root(uid=1000)
pam_unix(sudo:session): session closed for user root
analyst : PWD=/home/analyst ; USER=root ; COMMAND=/bin/id -u
pam_unix(sudo:session): session opened for user root(uid=0) by root(uid=1000)
pam_unix(sudo:session): session closed for user root
[root@rocky9-lab ~]# journalctl -p warning --since "-10min" --no-pager -o short-iso | tail -4
2026-09-24T12:19:30+0000 rocky9-lab systemd[1]: Failed to start Lab crashloop.
2026-09-24T12:19:35+0000 rocky9-lab systemd[1]: lab-crash.service: Start request repeated too quickly.
2026-09-24T12:19:35+0000 rocky9-lab systemd[1]: lab-crash.service: Failed with result 'exit-code'.
2026-09-24T12:19:35+0000 rocky9-lab systemd[1]: Failed to start Lab crashloop.
[root@rocky9-lab ~]# journalctl _COMM=sudo -n 1 -o verbose --no-pager | grep -E '^\s+(_PID|_UID|_COMM|_EXE|_CMDLINE|_SYSTEMD_UNIT|SYSLOG_IDENTIFIER|PRIORITY|MESSAGE)='
SYSLOG_IDENTIFIER=sudo
_UID=1000
_COMM=sudo
_EXE=/usr/bin/sudo
_SYSTEMD_UNIT=session-145.scope
PRIORITY=6
MESSAGE=pam_unix(sudo:session): session closed for user root
_PID=4573
_CMDLINE=sudo -n id -u
[root@rocky9-lab ~]# journalctl -u sshd -g 'Invalid user' -o json --no-pager | tail -1 | python3 -c 'import json,sys; d=json.loads(sys.stdin.read()); print({k:d[k] for k in ("_PID","_SYSTEMD_UNIT","PRIORITY","MESSAGE") if k in d})'
{'_PID': '4570', '_SYSTEMD_UNIT': 'sshd.service', 'PRIORITY': '6', 'MESSAGE': 'Invalid user test from 127.0.0.1 port 59360'}
[root@rocky9-lab ~]# journalctl -F _SYSTEMD_UNIT --no-pager | sort | head -8
auditd.service
broken.service
crond.service
dbus-broker.service
dnf-makecache.service
exit2.service
init.scope
lab-app1.service
[root@rocky9-lab ~]# journalctl -u sshd --since "-2min" -g 'Invalid user' -o cat --no-pager | awk '{print $3}' | sort | uniq -c
2 admin
2 oracle
2 test
| 관찰 | 의미 |
|---|---|
Invalid user oracle from 127.0.0.1 ... → Connection closed by invalid user oracle ... [preauth] | 존재하지 않는 계정 접속 시도. 인증 전 단계(preauth) 에서 끊겼다 |
Accepted publickey for root from 172.17.0.1 ... ED25519 SHA256:... | 성공한 로그인. 출발지 IP와 사용된 키의 지문 까지 남는다 |
-g 'Invalid user' -o cat → 3줄 | 패턴과 출력 형식을 조합하면 필요한 줄만 깔끔하게 뽑힌다 |
sudo: analyst : PWD=/home/analyst ; USER=root ; COMMAND=/bin/cat /etc/shadow | 누가, 어디서, 누구 권한으로, 무엇을 실행했는지 한 줄에 남는다 |
session opened for user root(uid=0) by root(uid=1000) | sudo 세션 시작. by ...(uid=1000)이 실제 요청자다 |
-p warning → lab-crash 관련 경고들 | 우선순위로 거르면 장애·보안 이벤트만 빠르게 볼 수 있다 (29편의 흔적) |
verbose: _UID=1000, _EXE=/usr/bin/sudo, _CMDLINE=sudo -n id -u, _SYSTEMD_UNIT=session-145.scope | 로그를 남긴 프로세스의 실제 계정·실행 파일·명령줄·로그인 세션 |
json 추출 {'_PID': '4570', '_SYSTEMD_UNIT': 'sshd.service', ...} | SIEM으로 보낼 때 이 형식을 쓴다 |
-F _SYSTEMD_UNIT → auditd, broken, exit2, lab-app1 ... | 이 서버에서 로그를 남긴 적 있는 모든 unit. 지금은 없는 unit(broken, exit2)도 흔적이 남는다 |
| 계정별 집계 admin 2, oracle 2, test 2 | 실습을 두 번 실행해 계정마다 2회씩 기록되었다. awk·uniq -c로 Brute Force 대상 계정 분포를 만든다 |
| 주제 | 내용 |
|---|---|
| 신뢰할 수 있는 필드 | 공격자가 logger "Accepted password for admin"으로 가짜 로그를 넣어도 _COMM=logger, _UID가 남아 위조를 구분 할 수 있다 |
| 흔적의 지속성 | -F _SYSTEMD_UNIT에는 삭제된 unit 이름도 남는다. 공격자가 지운 악성 서비스의 이름을 찾을 수 있다 |
| 로그 삭제 | root는 journal 파일을 지울 수 있다(--vacuum, 파일 삭제). 원격 전송 이 없으면 증거가 사라진다 (32편) |
| 접근 통제 | 일반 사용자는 자신의 로그만 본다. adm, systemd-journal, wheel 그룹은 전체를 본다 → 그룹 구성원 점검 필요 (33편 실측) |
[Detection] SIEM 경보: sshd "Invalid user" 5분간 200회 (단일 IP)
↓
[집계] journalctl -u sshd --since "-10min" -g 'Invalid user' -o cat | awk '{print $5}' | sort | uniq -c | sort -rn
journalctl -u sshd --since "-10min" -g 'Invalid user' -o cat | awk '{print $3}' | sort | uniq -c | sort -rn | head
↓
[성공 여부] journalctl -u sshd --since "-10min" -g 'Accepted' -o short-iso ← 같은 IP 의 성공이 있는가
↓
[후속 행위] 성공했다면: journalctl _COMM=sudo --since <로그인 시각>
journalctl _SYSTEMD_UNIT=session-N.scope ← 그 세션의 모든 기록
↓
[Response] 출발지 차단 · 계정 잠금 · 세션 종료 · 키/비밀번호 교체
| 자주 쓰는 한 줄 | 목적 |
|---|---|
journalctl -u sshd -g 'Failed password\|Invalid user' --since today | 오늘의 인증 실패 |
journalctl -u sshd -g 'Accepted' --since today -o short-iso | 오늘의 로그인 성공 |
journalctl _COMM=sudo -g 'COMMAND=' --since today -o cat | 오늘의 sudo 명령 |
journalctl -p err -b | 이번 부팅의 오류 |
journalctl _UID=48 --since "-1h" | 특정 계정(예: 웹 서버)이 남긴 로그 |
| 실수 | 결과 | 예방 |
|---|---|---|
journalctl \| grep만 사용 | 느리고 필드 활용 불가 | -u, _COMM=, -g 조합 |
| 일반 계정으로 조회 | 시스템 로그가 안 보여 "로그 없음"으로 오판 | root 또는 adm/systemd-journal 그룹 |
| 시간대를 확인하지 않음 | 로그(UTC)와 보고서 시간 불일치 | -o short-iso, --utc 명시 |
-u sshd로 Ubuntu 조회 | Ubuntu의 unit 이름은 ssh | -u ssh 또는 -t sshd |
| 긴 출력을 pager로 봄 | 스크립트가 멈춤 | --no-pager |
[ ] -u, --since, -p, -g 로 로그를 걸렀다
[ ] SSH Invalid user 와 Accepted 로그의 형식을 확인했다
[ ] sudo 로그에서 요청자·명령·대상 계정을 읽었다
[ ] -o verbose 로 _UID, _EXE, _CMDLINE, _SYSTEMD_UNIT 필드를 확인했다
[ ] -o json 으로 필드를 추출했다
[ ] 계정별 로그인 실패 횟수를 집계했다
_ 필드는 journald가 붙인 신뢰 가능한 값이다.-u(unit), --since(시간), -p(우선순위), -g(패턴), _필드=값(필드)으로 필요한 로그만 뽑는다.-o cat은 집계용, -o verbose는 분석용, -o json은 SIEM 연동용이다.다음 글 「32. journald 저장 설정과 로그 보존」 에서는 journal이 재부팅하면 사라질 수 있다 는 사실부터 확인한다. Storage=persistent로 영구 저장을 켜고, 용량·보존 기간 제한과 무결성 검증(--verify), 원격 전송 구성의 기준을 정리한다.