파일 · 권한 · 사용자 관리 33 / 50 · Part 4. 사용자와 그룹 관리
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (Docker 격리 컨테이너, 테스트 계정analyst)
기초편 연계: 「리눅스 시스템 기초」 18. Linux 사용자와 그룹
32편에서 /etc/passwd에는 비밀번호 해시가 없고 /etc/shadow를 참조한다고 했다. 이번 글에서 그 /etc/shadow를 연다.
/etc/shadow는 비밀번호 해시와 만료 정책 을 담은 파일이다. 이 파일이 중요한 이유는 두 가지다. 첫째, 해시가 유출되면 오프라인 크래킹으로 비밀번호를 알아낼 수 있다. 둘째, 계정의 잠금·만료 상태 가 여기에 있어 계정 보안 점검의 핵심 대상이다.
이번 글에서는 shadow의 필드를 해석하고, 해시 알고리즘 식별($6$ = SHA-512), 잠긴 계정 탐지, 무암호 계정 점검을 실습한다. (root 권한 실습이므로 격리된 테스트 컨테이너에서만 진행한다.)
| # | 필드 | 의미 |
|---|---|---|
| 1 | 계정명 | 사용자명 |
| 2 | 비밀번호 해시 | $id$salt$hash 또는 !/*/빈 값 |
| 3 | 마지막 변경 | 1970-01-01 이후 경과 일수 |
| 4 | 최소 사용일 | 변경 후 재변경까지 최소 일수 |
| 5 | 최대 사용일 | 비밀번호 유효 최대 일수 |
| 6 | 경고 일수 | 만료 전 경고 시작 일수 |
| 7 | 비활성 일수 | 만료 후 계정 비활성까지 |
| 8 | 만료일 | 계정 만료일(epoch 일수) |
| 9 | 예약 | 미사용 |
2번 필드의 $id$로 알고리즘을 안다.
$id$ | 알고리즘 | 비고 |
|---|---|---|
$6$ | SHA-512 | 현재 RHEL·구 Ubuntu 기본 |
$5$ | SHA-256 | |
$y$ | yescrypt | 최신 Debian·Ubuntu 기본 |
$1$ | MD5 | 구식·취약 |
| 2번 필드 | 의미 |
|---|---|
!/!!/*로 시작 | 로그인 잠김 (비밀번호 불가) |
| 빈 값 | 비밀번호 없이 로그인 — 매우 위험 |
$id$... | 정상 해시 |
passwd -l user는 해시 앞에 !를 붙여 잠근다. passwd -u로 해제한다.

/etc/shadow의 권한이 이 파일 보안의 전부다.
| 배포판 | 권한 | 접근 |
|---|---|---|
| Rocky/RHEL | 000 root:root | root만 (권한 검사 우회) |
| Ubuntu/Debian | 640 root:shadow | root + shadow 그룹 |
Rocky의 000은 "아무도 못 읽음"처럼 보이지만, root는 CAP_DAC_OVERRIDE로 권한 검사를 우회하므로(21편) 읽을 수 있다. 이 파일이 실수로 644가 되면 모든 사용자가 전 계정 해시를 읽어 오프라인 크래킹을 시도할 수 있다. 그래서 shadow 권한 점검은 CIS 벤치마크의 필수 항목이다.
비밀번호 검증 과정: 로그인 시 입력한 비밀번호에 저장된 salt를 붙여 같은 알고리즘으로 해시하고, 그 결과가 2번 필드의 hash와 일치하는지 비교한다. 해시는 단방향이라 저장된 값에서 원래 비밀번호를 복원할 수 없다(크래킹은 후보를 대입해 보는 것).
root 권한 실습.
noexpire는 실습용으로 비밀번호를 설정한 계정,locked-user는passwd -l로 잠근 계정이다.
# 1) 권한 확인
ls -l /etc/shadow
# 2) 필드 분해 (noexpire 계정)
getent shadow noexpire | awk -F: '{print "1 계정: "$1"\n2 해시: "substr($2,1,20)"...\n3 마지막변경(일): "$3"\n4 최소일수: "$4"\n5 최대일수: "$5"\n6 경고일수: "$6"\n7 비활성일수: "$7"\n8 만료일: "$8}'
# 3) 해시 알고리즘 식별
getent shadow noexpire | awk -F: '{print $2}' | cut -c1-3
# 4) 잠긴 계정 탐색
awk -F: '$2 ~ /^[!*]/ {print $1" -> "substr($2,1,3)}' /etc/shadow | head
# 5) 빈 비밀번호 계정 (위험)
awk -F: '$2=="" {print "빈 비밀번호: "$1}' /etc/shadow

텍스트 원본(실제 출력):
[root@rocky9-lab ~]# echo "=== /etc/shadow 권한 ==="
=== /etc/shadow 권한 ===
[root@rocky9-lab ~]# ls -l /etc/shadow
---------- 1 root root 701 Sep 24 14:43 /etc/shadow
[root@rocky9-lab ~]# echo "=== 필드 분해 ==="
=== 필드 분해 ===
[root@rocky9-lab ~]# getent shadow noexpire | awk -F: '{print "1 계정: "$1"\n2 해시: "substr($2,1,20)"...\n3 마지막변경(일): "$3"\n4 최소일수: "$4"\n5 최대일수: "$5"\n6 경고일수: "$6"\n7 비활성일수: "$7"\n8 만료일: "$8}'
1 계정: noexpire
2 해시: $6$93uXFXA/PIegiG.G$...
3 마지막변경(일): 20720
4 최소일수: 0
5 최대일수: 99999
6 경고일수: 7
7 비활성일수:
8 만료일:
[root@rocky9-lab ~]# echo "=== 해시 알고리즘 식별 ($id$) ==="
=== 해시 알고리즘 식별 ($) ===
[root@rocky9-lab ~]# getent shadow noexpire | awk -F: '{print $2}' | cut -c1-3
$6$
[root@rocky9-lab ~]# echo "=== 잠긴 계정 (! 또는 * 로 시작) ==="
=== 잠긴 계정 (! 또는 * 로 시작) ===
[root@rocky9-lab ~]# awk -F: '$2 ~ /^[!*]/ {print $1" -> "substr($2,1,3)}' /etc/shadow | head
root -> !lo
bin -> *
daemon -> *
adm -> *
lp -> *
sync -> *
shutdown -> *
halt -> *
mail -> *
operator -> *
[root@rocky9-lab ~]# echo "=== 비밀번호 없는 계정 (위험) ==="
=== 비밀번호 없는 계정 (위험) ===
[root@rocky9-lab ~]# awk -F: '$2=="" {print "빈 비밀번호: "$1}' /etc/shadow || echo "없음"
| 출력 | 해석 |
|---|---|
---------- 1 root root /etc/shadow | Rocky의 000 권한. root만 접근 |
2 해시: $6$93uXFXA/PIegiG.G$... | $6$ = SHA-512, 뒤에 salt |
3 마지막변경(일): 20720 | 1970 이후 20720일 ≈ 2026년. 최근 설정됨 |
5 최대일수: 99999 | 사실상 만료 없음(약 273년). 정책상 유한 값 권장 |
6 경고일수: 7 | 만료 7일 전 경고 |
$6$ (cut 결과) | 알고리즘 식별 |
root -> !lo | 이 컨테이너의 root는 !locked — 비밀번호 로그인 잠김(정상, 컨테이너 기본) |
bin -> *, daemon -> * 등 | 시스템 계정들. * = 로그인 불가 |
| 빈 비밀번호 계정 출력 없음 | 무암호 계정 없음 → 정상 |
root -> !lo가 눈에 띈다. 컨테이너 이미지의 root는 비밀번호 로그인이 잠겨 있다(!). 이는 정상이며, 실제 서버에서 root에 !가 있으면 "root 직접 로그인 차단" 정책일 수 있다.
| 점검 | 명령 | 위험 |
|---|---|---|
| shadow 권한 | stat -c '%a' /etc/shadow | 640/000이 아니면 해시 노출 |
| 무암호 계정 | awk -F: '$2==""' | 비밀번호 없이 로그인 |
| 약한 해시 | awk -F: '$2 ~ /^\$1\$/' | MD5 사용 |
| 만료 없는 정책 | 5번 필드 99999 | 정책상 유한값 권장(조직 기준) |
| 잠기지 않은 시스템 계정 | 서비스 계정에 유효 해시 | 서비스 계정은 잠금 |
주의: 만료일(99999)이 길다는 것만으로 "취약점"이라 단정하지 않는다. 조직 비밀번호 정책에 따라 판단한다. $6$(SHA-512)는 현재 안전한 알고리즘이며, $1$(MD5)이 진짜 위험 신호다.
[정기 점검] stat -c '%a %U:%G' /etc/shadow # 권한
awk -F: '$2==""{print "무암호:",$1}' /etc/shadow
awk -F: '$2 ~ /^\$1\$/{print "MD5:",$1}' /etc/shadow
awk -F: '$3>0 && $5!=99999{print $1,"만료정책",$5"일"}' /etc/shadow
↓
[Evidence] shadow 변경 시각(ctime), chage -l 로 계정별 만료 상태
↓
[Detection] /etc/shadow 읽기 시도(권한 없는 접근)를 auditd 로 기록
↓
[Response] 무암호 계정 즉시 잠금(passwd -l), 약한 해시 재설정
| 관점 | 내용 |
|---|---|
| 접근 감사 | shadow 읽기 시도는 해시 탈취 정황. auditd -w /etc/shadow -p r로 감시 |
| 크래킹 대비 | 해시 유출 시 피해 범위는 알고리즘 강도에 좌우. $6$/$y$ 사용 |
| 정책 | chage로 계정별 만료·경고 정책 관리(35편 연계) |
| 실수 | 결과 | 예방 |
|---|---|---|
| shadow 권한 변경(644 등) | 전 계정 해시 노출 | 000/640 유지 |
| shadow 직접 편집 | 손상·로그인 불가 | passwd·chage·usermod 사용 |
| MD5 해시 방치 | 크래킹 용이 | 최신 알고리즘으로 재설정 |
| 무암호 계정 방치 | 무인증 접근 | 즉시 잠금 |
root !를 침해로 오판 | 오탐 | root 직접 로그인 차단은 정상 정책일 수 있음 |
[ ] /etc/shadow 권한(000/640)을 확인했다
[ ] 9개 필드를 해석했다
[ ] $6$ 로 해시 알고리즘을 식별했다
[ ] 잠긴 계정(!, *)을 탐색했다
[ ] 무암호 계정 점검 명령을 안다
[ ] shadow 가 644 가 되면 위험한 이유를 설명할 수 있다
/etc/shadow는 해시와 만료 정책 을 담고, 권한은 000(Rocky)/640(Ubuntu)다.$id$로 알고리즘을 안다: $6$=SHA-512, $y$=yescrypt, $1$=MD5(취약).!/*로 시작하면 잠김, 빈 값이면 무암호 로그인 이다.644로 노출되면 전 계정 해시가 오프라인 크래킹에 노출된다.!는 정책에 따라 판단한다.passwd/chage/usermod로, 접근은 auditd로 감시한다.다음 글 「34. /etc/group 분석」 에서는 그룹 정보를 담은 /etc/group의 4개 필드를 해석하고, 기본 그룹과 보조 그룹의 차이, 그리고 wheel·sudo·docker 같은 위험 그룹의 구성원 점검을 실습한다.