이 글에서 다루는 내용

리눅스에서 sudo를 비밀번호 없이 실행하는 NOPASSWD 설정 방법부터, 그로 인해 발생하는 보안상의 취약점, 현재 계정이 어떤 권한을 가지고 있는지 확인하는 방법, 그리고 시스템의 사용자 목록을 조회하는 다양한 명령어를 다룹니다. 마지막으로는 실제 Raspberry Pi 서버에서 GitHub Actions self-hosted runner 계정에 적용된 sudoers 설정을 분석하며, 보안 원칙에 맞게 NOPASSWD를 "제대로 쓰는 법"을 구체적인 예시로 살펴봅니다.

NOPASSWD 설정 방법

sudoers 파일에 NOPASSWD 옵션을 추가하면 sudo 실행 시 비밀번호를 묻지 않습니다. 반드시 visudo 명령으로 편집해야 문법 오류로 인한 시스템 잠금을 피할 수 있습니다.

sudo visudo

별도 파일로 관리하려면 /etc/sudoers.d/ 아래에 생성합니다.

sudo visudo -f /etc/sudoers.d/mynopasswd

작성 규칙

특정 사용자 전체 허용:

username ALL=(ALL) NOPASSWD: ALL

특정 그룹 전체 허용:

%sudo ALL=(ALL) NOPASSWD: ALL

특정 명령만 비밀번호 없이 허용 (권장):

username ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/apt update

주의사항

/etc/sudoers.d/ 파일은 퍼미션이 0440이어야 하며, 파일명에 점(.)이나 틸드(~)가 있으면 무시됩니다. 설정 후 테스트는 다음처럼 합니다.

sudo -k           # 캐시된 인증 제거
sudo whoami       # 비밀번호 없이 바로 'root' 출력되면 성공

보안 관점에서의 위험성

sudo의 비밀번호 확인은 단순한 귀찮음이 아니라 2차 방어선 역할을 합니다. 사용자 계정이 탈취되었을 때 바로 root로 넘어가는 것을 막아주는 장벽이죠. NOPASSWD를 적용하면 이 방어선이 사라집니다.

구체적인 공격 시나리오

세션 하이재킹: 사용자가 터미널을 열어둔 채 자리를 비우거나 SSH 세션이 탈취되면 공격자가 즉시 sudo su로 root가 됩니다.

악성 스크립트 실행: 무심코 실행한 스크립트나 악성 패키지(npm, pip 등)가 sudo 명령을 포함하고 있으면 아무 저항 없이 시스템 전체를 장악합니다. 공급망 공격(supply chain attack)에서 특히 치명적입니다.

SSH 키 유출: 비밀번호 인증 없이 키로만 로그인하는 서버에서 키가 유출되면, 추가 인증 요소가 없어 바로 root 권한까지 연결됩니다.

셸 히스토리/환경변수 오염: .bashrcPATH를 조작해 sudo를 가짜 명령으로 바꿔치기하는 공격도 비밀번호 단계가 없으면 훨씬 수월해집니다.

실무적인 절충안

전부 허용하지 말고 다음 원칙을 따르는 것이 좋습니다.

첫째, 명령 화이트리스트. NOPASSWD: ALL 대신 꼭 필요한 명령만 지정합니다.

deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp

둘째, 인자까지 제한. 와일드카드 없이 정확한 명령을 지정해 다른 서비스까지 건드리지 못하게 막습니다.

셋째, 전용 계정 분리. 자동화용 계정(deploy, ci 등)을 따로 만들어 거기에만 NOPASSWD를 부여하고, 사람이 쓰는 계정은 정상적으로 비밀번호를 요구하게 둡니다.

넷째, 감사 로그 활성화. /etc/sudoersDefaults logfile=/var/log/sudo.log를 추가해 누가 언제 무엇을 실행했는지 기록합니다.

다섯째, 서버 자체 보안 강화. SSH 키 패스프레이즈 설정, fail2ban, 방화벽, 2FA 등으로 사용자 계정 단계에서 막는 층을 두껍게 만듭니다.

허용된 명령어 확인

현재 사용자가 sudo로 실행할 수 있는 명령어 목록을 확인하려면 다음 명령을 사용합니다.

sudo -l

출력 예시는 다음과 같습니다.

Matching Defaults entries for enes on yty-pi:
    env_reset, mail_badpass, secure_path=..., use_pty

User enes may run the following commands on yty-pi:
    (ALL : ALL) ALL

(ALL : ALL) ALL은 모든 사용자/그룹 권한으로 모든 명령을 실행할 수 있다는 의미입니다. 다만 NOPASSWD가 없으므로 비밀번호는 요구됩니다.

더 자세히 보기

규칙의 세부 옵션까지 표시:

sudo -ll

특정 명령을 실행할 수 있는지 확인:

sudo -l /usr/bin/systemctl restart myapp

다른 사용자의 권한 확인 (root 권한 필요):

sudo -l -U username

설정 파일 직접 확인

sudo cat /etc/sudoers
sudo ls /etc/sudoers.d/
sudo cat /etc/sudoers.d/*

실행 이력 확인

Ubuntu/Debian 계열:

sudo grep sudo /var/log/auth.log

RHEL/CentOS 계열:

sudo grep sudo /var/log/secure

systemd journal 사용 시:

journalctl _COMM=sudo

사용자 목록 조회

기본 명령

전체 사용자 목록:

cat /etc/passwd

사용자 이름만 깔끔하게:

cut -d: -f1 /etc/passwd
getent passwd | cut -d: -f1

getent/etc/passwd뿐 아니라 LDAP, NIS 등 외부 인증 소스까지 포함해서 조회합니다.

실제 사람 사용자만 필터링

시스템 계정을 제외하고 일반 계정만 보려면 UID로 거릅니다. 보통 UID 1000 이상이 일반 사용자입니다.

awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd

좀 더 보기 좋게:

awk -F: '$3 >= 1000 && $3 < 65534 {printf "%-15s UID=%-6s HOME=%s\n", $1, $3, $6}' /etc/passwd

현재 접속 중인 사용자

who         # 접속 중인 사용자와 터미널, 시간
w           # 더 자세히 (무엇을 하고 있는지까지)
users       # 이름만 한 줄로
last -a | head   # 최근 로그인 이력

그룹 정보

groups enes       # 특정 사용자의 그룹
id enes           # 더 자세한 정보
getent group sudo # sudo 그룹 멤버

실전 예시 분석 — GitHub Runner 계정

다음은 Raspberry Pi 서버(yty-pi)에서 GitHub Actions self-hosted runner용 계정에 설정된 실제 권한입니다.

User github-runner may run the following commands on yty-pi:
    (ALL) NOPASSWD: /bin/systemctl restart blog-spring, start blog-spring,
                    stop blog-spring, status blog-spring,
                    /bin/systemctl restart word-mov, start word-mov,
                    stop word-mov, status word-mov
    (enes) NOPASSWD: /usr/bin/pm2

1번 규칙 분석: systemd 서비스 제어

github-runner 계정은 비밀번호 없이 blog-springword-mov 두 개의 서비스만 제어할 수 있습니다. systemctl 전체가 아닌 특정 서비스명까지 명시했기 때문에 systemctl stop ssh 같은 위험한 명령은 차단됩니다. 허용 동작도 restart/start/stop/status로 한정되어 disable, mask, edit 같은 작업은 불가능합니다.

2번 규칙 분석: pm2 실행

(enes) NOPASSWD: /usr/bin/pm2

github-runnerenes 사용자 권한으로만 pm2를 실행할 수 있습니다(root가 아님).

sudo -u enes pm2 restart myapp

Node.js 앱은 일반 사용자 권한으로 돌리는 게 맞기 때문에, root로 확장되는 것을 막고 권한을 최소화한 좋은 설계입니다.

CI/CD 전체 흐름

설정으로 유추되는 파이프라인은 다음과 같습니다. GitHub에 코드를 푸시하면 GitHub Actions가 트리거되고, 서버의 self-hosted runner(github-runner 계정)가 작업을 받아 빌드한 뒤, Spring Boot 앱은 sudo systemctl restart로, Node.js 앱은 sudo -u enes pm2 restart로 배포하는 구조입니다.

개선 여지

Debian/Ubuntu 최신 버전(usr-merge 적용 후)에서는 실제 바이너리가 /usr/bin/systemctl이고 /bin/systemctl은 심볼릭 링크입니다. sudo는 경로 문자열을 그대로 매칭하므로, sudo /usr/bin/systemctl restart blog-spring으로 실행하면 규칙이 맞지 않아 비밀번호를 요구할 수 있습니다. 다음 명령으로 확인해봅시다.

ls -la /bin/systemctl /usr/bin/systemctl
readlink -f /bin/systemctl

또한 /usr/bin/pm2에 인자 제한이 없어서 pm2 kill, pm2 delete all 같은 파괴적인 명령도 가능합니다. 엄격하게 하려면 서비스명까지 명시하는 것이 좋습니다.

github-runner ALL=(enes) NOPASSWD: /usr/bin/pm2 restart blog-node, /usr/bin/pm2 reload blog-node

감사 로그를 분리하고 싶다면 sudoers.d 파일 상단에 다음 줄을 추가합니다.

Defaults:github-runner logfile=/var/log/sudo-github-runner.log

마치며

NOPASSWD는 단순히 "편하게 쓰는 기능"이 아니라 자동화 환경에서 불가피하게 필요한 도구입니다. 중요한 것은 NOPASSWD: ALL처럼 전부 열어두지 않고, 명령 화이트리스트, 인자 제한, 전용 계정 분리, 권한 최소화 원칙을 지키는 것입니다. 글에서 본 GitHub runner 예시처럼 잘 설계된 설정은 CI/CD 자동화의 편의성과 시스템 보안을 동시에 확보할 수 있습니다.