다음에 올 것을 만들어가는 기술에 대한 심층 기사.

sudo의 진화: Unix su에서 현대의 권한 관리까지

Unix su부터 doas, polkit, run0까지 sudo의 진화 과정과 보안 함정, 실전 권장 설정을 정리했습니다.

녹슨 철제 열쇠에서 매끈한 현대식 열쇠까지, 벨벳 벽에 전시된 열쇠들

여러분이 놀랄 만한 사실이 하나 있습니다. sudo에 비밀번호를 입력하면 아무런 피드백이 전혀 나오지 않습니다. 별표도, 점도, 아무것도 없이 커서만 가만히 있죠. 이 설계는 1980년대에 결정됐고, 40년이 넘도록 사용자들을 헷갈리게 해 왔습니다. 터미널이 멈췄다고 생각해서 비밀번호를 여러 번 다시 치거나, 뭔가 고장 났다고 여기는 일이 흔합니다. Ubuntu는 마침내 기본값으로 별표를 보여주기로 했습니다. 46년 동안 이어온 “소리 없는 비밀번호 입력” 전통이 끝난 셈이죠.

이 작은 변화는 더 큰 사실을 보여줍니다. 리눅스의 권한 상승(privilege escalation) 방식은 수십 년에 걸쳐 내려진 결정들이 모여 만들어진 패치워크입니다. 어떤 것은 훌륭했고, 어떤 것은 꽤 의문스러웠으며, 모두 하위 호환성의 무게를 지고 있습니다. 지금에 이르게 된 경위를 이해하면 오늘날 더 나은 보안 결정을 내리는 데 도움이 됩니다.

sudo 이전: su의 세계

원래 유닉스의 권한 상승 도구는 su였습니다. “substitute user”의 약자죠. 하는 일은 딱 하나였습니다. 현재 셸 세션 전체를 다른 사용자, 보통 root로 바꾸는 것입니다. su를 입력하고 root 비밀번호를 넣은 다음 관리 작업을 하고, 끝나면 일반 계정으로 다시 나오는 방식입니다.

$ su
Password: [type root's password]
# whoami
root
# apt install nginx
# exit
$ whoami
normaluser

유닉스 시스템이 소수의 신뢰할 만한 운영자 규모를 넘어 커지면서 su의 문제가 분명해졌습니다. 관리자 권한이 필요한 모든 사람이 root 비밀번호를 알아야 했습니다. 팀원이 떠나면 root 비밀번호를 바꾸고 새 비밀번호를 나머지 모두에게 다시 나눠줘야 했죠. 감사 추적(audit trail)도 없었습니다. 일단 root가 되면 모든 작업이 “root”로 기록될 뿐, 실제로 누가 무엇을 실행했는지는 남지 않았습니다. 게다가 su는 완전한 root 셸을 줬기 때문에 오타 하나로 시스템이 날아갈 수 있었습니다.

고전적인 공포 이야기가 있습니다. 관리자가 rm -rf /tmp/old_files를 입력하려다가 rm -rf /를 치고 엔터를 누른 겁니다. 완전한 root 셸에서는 그 명령을 막을 장치가 전혀 없습니다. su는 “이 사람은 nginx만 재시작하면 된다”와 “이 사람은 시스템 전체에 무제한 접근이 필요하다”를 구분하지 않았습니다.

sudo가 root 비밀번호 문제를 해결한 방법

sudo(“superuser do”)는 1980년 SUNY Buffalo에서 Bob Coggeshall과 Cliff Spencer가 만들었습니다. 핵심 아이디어는 단순하지만 판도를 바꿨습니다. root 비밀번호를 공유하는 대신, 개별 사용자가 자신의 비밀번호로 특정 명령을 root 권한으로 실행하게 한 것입니다. 시스템 관리자는 설정 파일을 통해 누가 무엇을 실행할 수 있는지 통제합니다.

이 방식은 여러 문제를 한 번에 해결했습니다:

  • 공유 root 비밀번호 없음. 각 사용자는 자신의 자격 증명으로 인증합니다. 누군가 떠나면 sudo 권한만 제거하면 됩니다. 비밀번호를 바꿀 필요가 없습니다.
  • 세분화된 권한. 개발자에게 전체 root 권한을 주지 않고도 특정 서비스만 재시작하도록 허용할 수 있습니다.
  • 감사 추적. 모든 sudo 명령은 누가, 무엇을, 언제 실행했는지와 함께 기록됩니다.
  • 임시 권한 상승. 무제한 root 셸 대신, sudo는 상승된 권한으로 명령 하나만 실행하고 다시 일반 권한으로 돌아갑니다.
# Run a single command as root
$ sudo apt install nginx
[sudo] password for priya:
# That's YOUR password, not root's
# The audit trail in /var/log/auth.log:
# Mar 20 14:23:01 web-03 sudo: priya : TTY=pts/0 ;
#   PWD=/home/priya ; USER=root ; COMMAND=/usr/bin/apt install nginx

sudoers 파일: 강력하지만 위험한

sudo 설정은 /etc/sudoers에 있으며, 그 문법은 리눅스 관리에서 가장 헷갈리는 것 중 하나입니다. 문법 오류 하나면 sudo 자체에서 잠길 수 있습니다. 그래서 visudo 명령이 있는 겁니다. 저장하기 전에 파일을 검증해 주죠.

# /etc/sudoers syntax:
# WHO  WHERE = (AS_WHOM) WHAT
# Let priya run anything as root on any host
priya ALL=(ALL:ALL) ALL
# Let the 'webdev' group restart nginx only
%webdev ALL=(root) /usr/bin/systemctl restart nginx,\
/usr/bin/systemctl reload nginx
# Let deploy user run deploys without a password
deploy ALL=(root) NOPASSWD: /opt/deploy/run.sh
# DANGEROUS: This looks restrictive but isn't
bob ALL=(root) /usr/bin/vim
# Bob can now run: sudo vim, then :!bash to get a root shell

마지막 예시, 즉 누군가에게 vim에 대한 sudo 권한을 주는 것은 사람들이 끊임없이 빠지는 보안 함정입니다. Vim(그리고 다른 많은 프로그램)은 셸 명령을 띄울 수 있습니다. 사용자가 sudo vim을 실행할 수 있다면, 사실상 무제한 root 접근 권한을 가진 셈입니다. less, man, awk, find, python 등 수십 개의 다른 명령도 마찬가지입니다. GTFOBins 프로젝트는 제한된 셸을 탈출하는 데 쓰일 수 있는 바이너리를 망라한 목록을 관리합니다.

sudo의 기이한 동작들과 보안 함정

sudo는 40년이 넘는 역사 동안 꽤 이상한 동작들을 쌓아 왔습니다. 보안을 위해서는 이런 특성을 이해하는 것이 중요합니다:

  • 자격 증명 캐싱. 비밀번호를 입력하면 sudo는 기본적으로 15분 동안 이를 캐시합니다. 그 시간 동안에는 어떤 sudo 명령도 인증 없이 실행됩니다. 잠기지 않은 터미널을 그대로 두고 자리를 비우면, 누구든 최대 15분 동안 sudo 명령을 실행할 수 있습니다. 캐시를 즉시 지우려면 sudo -k를 실행하세요.
  • tty_tickets 설정. 기본적으로 sudo의 자격 증명 캐시는 터미널별로 분리됩니다. 하지만 일부 시스템에서는 한 터미널에서 인증하면 다른 모든 터미널의 sudo도 풀립니다. Defaults 설정을 확인하세요.
  • 환경 변수. sudo는 대부분의 환경 변수를 정리하지만 전부는 아닙니다. LD_PRELOAD와 LD_LIBRARY_PATH는 제거되지만, env_keep이 잘못 설정되어 있으면 공격자가 악성 라이브러리 경로를 주입할 수 있습니다. 이는 실제 권한 상승 익스플로잇 여러 건의 근간이 되었습니다.
  • NOPASSWD의 함정. NOPASSWD는 자동화에 편리하지만 너무 넓게 적용하면 위험합니다. NOPASSWD sudo 권한을 가진 사용자로 실행되는 침해된 애플리케이션은 사실상 root를 가진 것과 같습니다. 크랙할 비밀번호조차 없으니까요.
# Security hardening for sudoers:
# Require password every time (disable caching)
Defaults timestamp_timeout=0
# Or reduce cache to 1 minute
Defaults timestamp_timeout=1
# Ensure credential cache is per-terminal
Defaults tty_tickets
# Log all sudo I/O (records full terminal sessions)
Defaults log_input, log_output
Defaults iolog_dir=/var/log/sudo-io
# Require root password instead of user password
# (prevents compromised user accounts from sudo-ing)
Defaults rootpw
# Show asterisks when typing password
Defaults pwfeedback

현대적인 대안: doas, polkit, run0

sudo의 복잡성은 저마다 다른 철학을 가진 여러 대안을 낳았습니다.

doas: 복잡성 없는 sudo

OpenBSD의 doas(“dedicated OpenBSD application subexecutor”)는 2015년 Ted Unangst가 만들었습니다. sudo가 감사하기에 너무 복잡해졌기 때문이죠. 설정 전체가 보통 2~3줄이면 충분합니다:

# /etc/doas.conf — the entire configuration
permit persist priya
permit nopass deploy as root cmd /opt/deploy/run.sh
# That's it. Compare this to a typical sudoers file.
# 'persist' is like sudo's credential caching
# 'nopass' is like NOPASSWD

doas 소스 코드는 C로 약 2,500줄입니다. sudo는 15만 줄이 넘습니다. 명령별 인자 매칭이나 LDAP 연동 같은 sudo의 고급 기능이 필요 없는 시스템이라면, doas는 설정하고 감사하고 보안을 확보하기가 훨씬 간단합니다. 저는 개인 서버에서 몇 년째 doas를 쓰고 있는데, sudo가 그립다는 생각은 한 번도 없었습니다.

polkit: 세밀한 데스크톱 권한

PolicyKit(polkit)은 완전히 다른 접근을 택합니다. 개별 명령을 감싸는 대신, 애플리케이션이 요청할 수 있는 액션을 정의합니다. 데스크톱 앱이 네트워크 설정을 바꿔야 하면 polkit에 권한을 요청하고, polkit은 규칙에 따라 허용, 거부, 또는 사용자에게 확인을 요청할지 결정합니다.

리눅스 데스크톱에서 비밀번호 없이 USB 드라이브를 마운트하면서도 소프트웨어를 설치하려면 인증이 필요한 것이 바로 이 방식 덕분입니다. 세분화 단위가 명령이 아니라 액션 수준이라서, 데스크톱 사용자가 실제로 권한을 생각하는 방식과 더 잘 맞습니다.

run0: systemd의 급진적 재설계

systemd의 run0은 가장 새로운 도구이며, sudo와 근본적으로 다르게 동작합니다. 기존 세션에서 명령을 root로 실행하는 대신(setuid 비트가 필요하고 보안 골칫거리를 낳습니다), run0은 서비스 관리자에게 root로 실행되는 새 서비스를 띄워 달라고 요청합니다. 사용자 세션은 권한이 상승되지 않고, 특권 프로세스는 완전히 별개입니다.

이 방식은 sudo 취약점 범주 전체를 우회합니다. setuid 바이너리도, 자격 증명 캐싱도, 환경 변수 주입 경로도 없습니다. 대가로 systemd가 필요하다는 점이 있어서 BSD 시스템이나 최소 구성의 리눅스에서는 쓸 수 없습니다. 하지만 systemd 기반 서버라면 아마도 현존하는 가장 안전한 선택지일 겁니다.

실전 권장 사항

수년간 리눅스 서버를 관리하고 권한 상승 사고를 조사해 오면서, 제가 실제로 권하는 것은 다음과 같습니다:

  1. root 로그인을 완전히 비활성화하세요. 모든 작업에 sudo나 doas를 쓰세요. 공유 root 비밀번호는 있어서는 안 됩니다.
  2. sudoers에서는 개별 사용자보다 그룹을 쓰세요. 그룹 멤버십으로 접근을 관리하세요. 누군가 떠나면 그룹에서만 빼면 됩니다.
  3. 인터프리터나 에디터에는 sudo 권한을 절대 주지 마세요. sudo vim, sudo python, sudo less는 안 됩니다. root 소유 파일을 편집해야 한다면 대신 sudoedit을 쓰세요.
  4. NOPASSWD 규칙을 최소화하세요. 자동화된 프로세스에만 쓰고, 그것도 전체 경로를 지정한 특정 명령에만 적용하세요.
  5. sudo 로깅을 활성화하세요. 최소한 모든 sudo 명령은 기록하세요. 민감한 시스템이라면 전체 I/O 로깅을 켜세요.
  6. 단순한 시스템에는 doas를 고려하세요. LDAP 연동이나 복잡한 매칭 규칙이 필요 없다면, doas가 제대로 설정하기 더 쉽습니다.
  7. 자격 증명 캐시를 적극적으로 줄이세요. timestamp_timeout을 1~5분으로 줄이거나, 운영 서버에서는 캐싱을 아예 끄세요.

최선의 권한 상승 메커니즘은 필요한 최소한의 접근 권한을, 필요한 최소 시간 동안, 완전한 감사 추적과 함께 부여하는 것입니다. 나머지는 모두 타협입니다.

sudo는 어디로도 가지 않습니다. 스크립트, 자동화, 문서, 그리고 손에 밴 습관 속에 너무 깊이 박혀 있어서 사라질 수 없습니다. 하지만 대안의 지형은 그 어느 때보다 건강합니다. sudo를 제대로 강화해서 계속 쓰든, 단순함을 위해 doas로 바꾸든, 보안이 중요한 시스템이라면 run0를 도입하든, 중요한 것은 권한 상승 도구가 실제로 무엇을 하는지 이해하는 것입니다. 사람들이 sudo가 그렇게 한다고 생각하는 것과 sudo가 실제로 하는 것 사이의 간극에서 대부분의 보안 사고가 발생하니까요.