파일 · 권한 · 사용자 관리 14 / 50 · Part 2. Linux 파일 종류와 속성
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (Docker 격리 컨테이너, 테스트 계정analyst)
기초편 연계: 「리눅스 시스템 기초」 14. inode와 파일 시스템
13편에서 inode에는 이름이 없고 이름은 디렉터리 엔트리에 있다는 것을 확인했다. 그렇다면 하나의 inode에 이름을 두 개 붙일 수 있을까? 가능하다. 그것이 하드 링크다. 반면 심볼릭 링크는 이름이 아니라 "대상 경로를 적어 둔 작은 파일" 이다.
두 링크는 겉보기에 비슷하지만 동작은 완전히 다르다. 이번 글에서는 원본 이름을 바꿨을 때 두 링크가 어떻게 반응하는지, 파일 시스템 경계와 디렉터리에서 어떤 제약이 있는지, 그리고 링크가 권한 검사를 우회하지 못한다 는 것을 실습으로 확인한다.
ln 원본 새이름ln -s 대상 링크이름| 항목 | 하드 링크 | 심볼릭 링크 |
|---|---|---|
| inode | 원본과 같음 | 별도 |
| 원본 이름 변경·삭제 | 영향 없음 | 링크 끊어짐 |
| 다른 파일 시스템 | 불가 | 가능 |
| 디렉터리 대상 | 불가 (루프 방지) | 가능 |
| 크기 표시 | 원본 크기 | 경로 문자열 길이 |

디렉터리 엔트리는 "이름 → inode 번호"만 저장한다. inode 번호는 파일 시스템 안에서만 의미가 있으므로(13편), 다른 파일 시스템의 inode를 가리킬 방법이 없다. 커널은 EXDEV(Invalid cross-device link)를 반환한다.
디렉터리에 하드 링크를 허용하면 a/b/→a 같은 순환이 생겨 find, du, 파일 시스템 검사 도구가 무한 루프에 빠질 수 있고, ..이 가리킬 상위가 여러 개가 된다. 그래서 커널은 .과 .. 외의 디렉터리 하드 링크를 허용하지 않는다.
lrwxrwxrwx로 보이지만 이 권한은 사용되지 않는다. 커널은 링크를 따라간 대상 파일의 권한 을 검사한다.즉, 링크는 권한 우회 수단이 아니다. 위험은 다른 곳에 있다. root 권한 프로세스가 사용자가 만든 링크를 따라가 파일을 쓰거나 권한을 바꾸는 경우다. 이를 막기 위해 커널에는 fs.protected_symlinks와 fs.protected_hardlinks 보호 설정이 있다. 커널 자체 기본값은 0이지만, Rocky와 Ubuntu는 systemd 기본 설정(/usr/lib/sysctl.d/50-default.conf 등)으로 부팅 시 1(활성)을 적용한다.
cd /data && mkdir lab14 && cd lab14
# 1) 하드 링크와 심볼릭 링크 만들기
echo "original" > target.txt
ln target.txt hard.txt
ln -s target.txt soft.txt
ls -li
# 2) 하드 링크로 수정하면 원본도 바뀐다 (같은 inode)
echo "appended via hard" >> hard.txt && cat target.txt
# 3) 원본 이름을 바꾸면?
mv target.txt moved.txt
cat hard.txt
cat soft.txt
ls -l soft.txt
readlink soft.txt ; readlink -f soft.txt
# 4) 같은 inode를 가진 모든 이름 찾기
find . -samefile moved.txt
# 5) 제약 확인
ln /data/lab14/moved.txt /dev/shm/cross.txt # 다른 파일 시스템
ln /data/lab14 /data/dirlink # 디렉터리
# 6) 링크는 권한을 우회하지 못한다
ln -s /etc/shadow shadow-link && ls -l shadow-link && cat shadow-link

텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ cd /data && mkdir lab14 && cd lab14
[analyst@rocky9-lab lab14]$ echo "original" > target.txt
[analyst@rocky9-lab lab14]$ ln target.txt hard.txt
[analyst@rocky9-lab lab14]$ ln -s target.txt soft.txt
[analyst@rocky9-lab lab14]$ ls -li
total 8
12 -rw-r--r-- 2 analyst analyst 9 Sep 24 10:35 hard.txt
13 lrwxrwxrwx 1 analyst analyst 10 Sep 24 10:35 soft.txt -> target.txt
12 -rw-r--r-- 2 analyst analyst 9 Sep 24 10:35 target.txt
[analyst@rocky9-lab lab14]$ echo "appended via hard" >> hard.txt && cat target.txt
original
appended via hard
[analyst@rocky9-lab lab14]$ mv target.txt moved.txt
[analyst@rocky9-lab lab14]$ cat hard.txt
original
appended via hard
[analyst@rocky9-lab lab14]$ cat soft.txt
cat: soft.txt: No such file or directory
[analyst@rocky9-lab lab14]$ ls -l soft.txt
lrwxrwxrwx 1 analyst analyst 10 Sep 24 10:35 soft.txt -> target.txt
[analyst@rocky9-lab lab14]$ readlink soft.txt ; readlink -f soft.txt
target.txt
/data/lab14/target.txt
[analyst@rocky9-lab lab14]$ find . -samefile moved.txt
./moved.txt
./hard.txt
[analyst@rocky9-lab lab14]$ ln /data/lab14/moved.txt /dev/shm/cross.txt
ln: failed to create hard link '/dev/shm/cross.txt' => '/data/lab14/moved.txt': Invalid cross-device link
[analyst@rocky9-lab lab14]$ ln /data/lab14 /data/dirlink
ln: /data/lab14: hard link not allowed for directory
[analyst@rocky9-lab lab14]$ ln -s /etc/shadow shadow-link && ls -l shadow-link && cat shadow-link
lrwxrwxrwx 1 analyst analyst 11 Sep 24 10:35 shadow-link -> /etc/shadow
cat: shadow-link: Permission denied
| 출력 | 해석 |
|---|---|
12 ... 2 ... hard.txt / 12 ... 2 ... target.txt | 같은 inode 12, 링크 수 2 |
13 lrwxrwxrwx ... soft.txt -> target.txt 크기 10 | 별도 inode 13, 크기는 target.txt 문자열 길이 |
cat target.txt에 추가 내용 표시 | 하드 링크로 쓴 내용이 원본 이름으로도 보임 (같은 데이터) |
이름 변경 후 cat hard.txt 정상 | 하드 링크는 이름과 무관하게 inode를 직접 가리킴 |
cat soft.txt → No such file or directory | 심볼릭 링크는 target.txt라는 경로 를 찾는데 그 이름이 사라짐 |
readlink → target.txt / readlink -f → /data/lab14/target.txt | 링크 내용 / 절대경로로 해석한 대상 (대상이 없어도 경로는 계산됨) |
find -samefile → hard.txt, moved.txt | 같은 inode를 가진 이름을 모두 찾음 |
Invalid cross-device link | ext4(/data) → tmpfs(/dev/shm) 하드 링크 불가 |
hard link not allowed for directory | 디렉터리 하드 링크 금지 |
shadow-link -> /etc/shadow 생성 성공, cat → Permission denied | 링크는 누구나 어디로든 만들 수 있지만, 읽기 권한은 대상 /etc/shadow가 결정한다 |
마지막 줄이 보안 관점의 핵심이다. 링크 생성 자체는 권한 상승이 아니다. 문제는 권한이 높은 프로세스가 링크를 따라갈 때 다.
| 주제 | 설명 | 대응 |
|---|---|---|
| 심볼릭 링크 경쟁(TOCTOU) | root 스크립트가 /tmp/report.txt에 쓰려는 순간, 그 이름이 사용자가 만든 링크로 바뀌어 있으면 링크 대상 파일이 root 권한으로 덮어써질 수 있다 | mktemp 사용, fs.protected_symlinks=1 유지 |
| 하드 링크 고정 | 사용자가 SUID 파일에 하드 링크를 만들어 두면 패키지 업데이트 후에도 취약한 옛 버전 inode가 남는다 | fs.protected_hardlinks=1 (본인 소유가 아니고 읽기·쓰기 권한도 없는 파일에는 하드 링크 금지) |
| 로그 무력화 | 로그·히스토리 파일을 /dev/null 링크로 교체 | find / -type l -lname /dev/null |
| 웹 루트 링크 | 업로드 디렉터리에 /etc 등을 가리키는 링크 → 웹 서버가 따라가면 정보 노출 | 웹 서버의 심볼릭 링크 추종 옵션 제한 (FollowSymLinks 등) |
| 링크를 따라간 권한 변경 | root가 chown -R/chmod -R 실행 시 링크 대상 변경 위험 | -h 옵션, find -type f로 대상 한정 |
두 커널 보호 설정의 동작:
| 설정 | 값 1일 때 |
|---|---|
fs.protected_symlinks | Sticky Bit가 있는 공용 디렉터리(/tmp)에서, 링크 소유자와 따라가는 프로세스가 다르고 디렉터리 소유자도 아니면 따라가기 거부 |
fs.protected_hardlinks | 본인 소유가 아니고 읽기·쓰기 권한도 없는 파일에 대해 하드 링크 생성 거부 |
[Detection] FIM: /var/www/html/upload/ 에 심볼릭 링크 생성 (→ /etc)
↓
[Evidence] ls -la → cfg -> /etc (owner=www-data)
stat cfg → birth 02:31:07
↓
[판단] 웹 계정이 업로드 폴더에 시스템 경로 링크 생성 → 파일 노출 시도
↓
[Investigation] 웹 로그: GET /upload/cfg/passwd 요청 여부와 응답 코드
↓
[Response] 링크 제거, 웹 서버 링크 추종 설정 점검, 업로드 기능 확인
| 관점 | 내용 |
|---|---|
| 점검 | sysctl fs.protected_symlinks fs.protected_hardlinks 값이 1인지 확인 |
| 탐지 | 공용·웹 디렉터리의 신규 심볼릭 링크, /dev/null을 가리키는 로그 파일 |
| IOC 분석 | 하드 링크는 이름이 달라도 같은 inode. 악성 파일 삭제 후에도 다른 이름이 남아 있는지 find -samefile로 확인 |
| Evidence | 링크는 ls -l의 -> 뒤 경로와 링크 자체의 stat(생성 시각·소유자)을 함께 기록 |
| 실수 | 결과 | 예방 |
|---|---|---|
| 상대경로 심볼릭 링크를 다른 위치로 이동 | 링크 끊어짐 | 필요한 경우 절대경로 링크 |
| 악성 파일 한 이름만 삭제 | 하드 링크로 다른 이름이 남음 | 링크 수 확인 후 find -samefile |
lrwxrwxrwx를 보고 "권한 과다"로 판단 | 오탐 | 링크 권한은 사용되지 않음, 대상 권한 확인 |
cp -r로 링크 포함 디렉터리 백업 | 대상 내용이 복사되거나 순환 | cp -a, rsync -a |
| 보호 설정을 끄고 문제 해결 | 링크 공격에 노출 | 애플리케이션 경로 설계 수정 |
[ ] 하드 링크와 원본의 inode 가 같음을 확인했다
[ ] 원본 이름 변경 시 하드 링크는 유지, 심볼릭 링크는 끊어짐을 확인했다
[ ] find -samefile 로 같은 inode 의 이름을 모두 찾았다
[ ] 다른 FS·디렉터리 하드 링크가 거부됨을 확인했다
[ ] /etc/shadow 링크가 권한을 우회하지 못함을 확인했다
[ ] fs.protected_symlinks / protected_hardlinks 의 역할을 설명할 수 있다
protected_symlinks, protected_hardlinks)이 이를 완화한다.다음 글 「15. Linux 파일 timestamp 이해하기 (atime·mtime·ctime)」 에서는 네 가지 시간이 각각 어떤 동작에서 바뀌는지 를 하나씩 실험하고, 파일을 두 번 읽어도 atime이 한 번만 바뀌는 relatime 정책을 확인한다.