리눅스 capability로 최소 권한 구조 만들기

리눅스 전통 모델에서는 UID가 0이면 거의 모든 특권을 가지는 슈퍼유저이고, 그 외 사용자는 커널이 정의한 각종 제한을 그대로 적용받는다. capability는 이 거친 이분법을 깨고, 특권을 기능 단위로 쪼개 프로세스에 선택적으로 부여하기 위해 도입된 기능이다.(레드햇)

결과적으로 다음과 같은 목표를 달성할 수 있다.

  • 원래는 root만 할 수 있던 일을 일반 사용자 프로세스에 제한적으로 허용
  • 필요한 권한만 골라 주어 침해 사고 발생 시 피해 범위를 줄임
  • setuid 바이너리 의존도를 줄여 보안을 강화

이 글에서는 capability의 기본 개념을 간단히 정리한 뒤, setcap과 getcap으로 파일 capability를 설정하는 방법, 대표적인 최소 권한 예시, systemd와 연계한 서비스 단위 적용 전략까지 실제 운영 환경에서 바로 쓸 수 있는 내용을 중심으로 살펴본다.

리눅스 capability 개념과 권한 세트 구조

리눅스 커널은 각 프로세스에 대해 여러 개의 capability 세트를 관리한다. 주요 세트는 대략 다음과 같다.(ilManzo’s blog)

  • Permitted 세트
    • 이 프로세스가 잠재적으로 가질 수 있는 최대 권한 범위
    • 여기서 빠진 capability는 다시 얻을 수 없다
  • Effective 세트
    • 지금 실제로 활성화되어 권한 체크에 사용되는 capability 집합
  • Bounding 세트
    • exec 시 파일 capability에서 가져올 수 있는 최대 한계를 정의
  • Ambient 세트
    • 비특권 바이너리를 exec할 때도 일정 권한을 유지하는 용도로 도입된 세트

파일 자체도 별도의 capability를 가질 수 있다. 파일 capability는 해당 바이너리가 exec될 때 프로세스의 Permitted Effective 세트에 어떤 capability를 추가할지 정의한다.(Man7)

중요한 포인트는 capability가 유저 계정이 아니라 실행 파일과 실행 중인 프로세스에 붙는 속성이라는 점이다. 덕분에 같은 사용자라도 어떤 프로그램을 실행하느냐에 따라 권한 수준을 세밀하게 구분할 수 있다.

최소 권한 설계 관점에서 capability의 의미

최소 권한 원칙은 한 문장으로 요약하면 이렇다. 해야 할 일을 수행하는 데 필요한 권한만 부여하고 그 이상은 주지 않는다. capabilities는 이 원칙을 리눅스 커널 수준에서 직접 구현할 수 있게 해 준다.(Medium)

예를 들어 웹 서버가 80번 포트에 바인딩할 수 있는 권한만 있으면 된다면, 굳이 파일 시스템 전체를 마음대로 조작할 수 있는 root 권한을 줄 이유가 없다. CAP_NET_BIND_SERVICE capability 하나만 주면 된다.(Man7)

이처럼 권한을 세분화해서 부여하면 다음과 같은 효과를 얻는다.

  • 취약점이 터져도 공격자가 할 수 있는 행동이 제한된다
  • setuid root 바이너리보다 위협 모델이 단순해진다
  • 보안 감사 시 어떤 프로그램에 어떤 권한이 있는지 명확하게 파악 가능

setcap과 getcap으로 파일 capability 다루기

파일 capability를 설정하고 확인하는 대표적인 도구가 setcap과 getcap이다. 이들은 보통 libcap 패키지에 포함되어 있으며, 루트 또는 CAP_SETFCAP를 가진 프로세스만 파일 capability를 수정할 수 있다.(Man7)

기본 사용 패턴은 다음과 같다.

# 파일의 capability 설정
sudo setcap 'cap_net_bind_service=ep' /usr/local/bin/myapp

# 파일의 capability 조회
getcap /usr/local/bin/myapp

# capability 제거
sudo setcap -r /usr/local/bin/myapp

여기서 문자열에서 e는 Effective, p는 Permitted 세트를 의미한다. 즉 위 예시는 myapp 바이너리 실행 시 cap_net_bind_service 권한을 Permitted와 Effective 양쪽에 부여하겠다는 뜻이다.(Medium)

여러 capability를 한 번에 줄 수도 있다.

sudo setcap 'cap_net_bind_service=ep cap_sys_time=ep' /usr/local/bin/myapp

이제 이 바이너리를 실행하는 프로세스는 1024 미만 포트 바인딩과 시스템 시간 변경 권한을 갖게 된다. 대표적인 최소 권한 설계에서는 이런 식으로 꼭 필요한 capability 목록만 엄선하는 작업이 핵심이다.

대표 capability 예시로 보는 최소 권한 적용

capabilities 목록은 꽤 방대하지만, 일반적인 서버 운영에서 자주 등장하는 것들만 추려 보면 다음과 같다.(Man7)

  • CAP_NET_BIND_SERVICE
    • 1024 미만 포트에 바인딩할 수 있는 권한
    • 웹 서버나 프록시를 비루트 계정으로 띄울 때 가장 많이 사용
  • CAP_NET_ADMIN
    • 라우팅 테이블 변경 등 네트워크 설정 변경 권한
    • 가능한 한 피하는 것이 좋을 정도로 강력
  • CAP_SYS_TIME
    • 시스템 시각 변경
    • NTP 데몬 등에만 제한적으로 부여
  • CAP_SYS_ADMIN
    • 매우 광범위한 특권 묶음을 포함하는 위험한 capability
    • 사실상 반쪽짜리 root에 가깝기 때문에 가급적 사용을 피한다

실전에서 최소 권한 설계를 하려면 애플리케이션이 실제로 어떤 시스템 콜과 기능을 사용하는지 먼저 파악하고, 그에 대응되는 capability만 골라서 주는 식으로 접근해야 한다.

예시 비루트 웹 서버에 포트 권한만 부여하기

가장 자주 언급되는 예시를 실제로 풀어 보면 이해가 쉽다.

가정 상황은 이렇다.

  • go로 작성된 웹 서버 바이너리 myweb이 있다
  • 리눅스 계정 webuser로 실행하고 싶다
  • 다만 80번 포트에 직접 바인딩해야 한다

기본적으로 1024 미만 포트는 root만 바인딩할 수 있기 때문에, 그냥 실행하면 권한 오류가 난다. 이때 파일 capability를 이용해 다음과 같이 처리할 수 있다.

sudo setcap 'cap_net_bind_service=ep' /usr/local/bin/myweb
sudo -u webuser /usr/local/bin/myweb

이제 myweb 프로세스는 webuser 계정으로 동작하면서도 80번 포트에 바인딩할 수 있다. 시스템 입장에서는 이 프로세스가 CAP_NET_BIND_SERVICE 하나만 가진 제한된 특권 프로세스가 되는 셈이다.(Baeldung on Kotlin)

중요한 점은 파일을 새로 빌드하거나 교체하면 capability가 사라질 수 있다는 것이다. 패키지 업데이트나 배포 스크립트에 setcap 작업을 포함시켜 두는 이유가 여기에 있다.

ambient capability를 이용한 헬퍼 프로세스 권한 전달

조금 더 복잡한 상황을 생각해 보자. 이미 특정 서비스 프로세스가 필요한 capability를 가지고 있고, 이 서비스가 별도의 헬퍼 프로그램을 exec해서 사용해야 하는 경우다.

기존 inheritable 세트만으로는 이 문제를 풀기 어려워서, 커널은 ambient capability 세트를 도입했다. ambient 세트에 들어 있는 capability는 비특권 바이너리를 exec하더라도 유지되며, 새로운 프로세스의 Permitted Effective 세트에 자동으로 반영된다.(LWN.net)

이 기능은 주로 다음과 같은 시나리오에서 유용하다.

  • systemd가 서비스 프로세스에 일부 capability를 넘겨주고
  • 서비스가 다시 헬퍼 바이너리를 exec해야 하는 경우

이때 파일마다 일일이 setcap을 걸지 않고도 전체 체인을 따라 capability를 유지할 수 있어, 배포와 버전 교체가 잦은 환경에서 관리 부담을 크게 줄여 준다.

systemd와 capability 연동으로 서비스 단위 최소 권한 구성

요즘 배포판에서는 개별 바이너리에 setcap을 직접 거는 대신 systemd 서비스 유닛에서 capability를 제어하는 방식이 많이 쓰인다. systemd는 내부적으로 cgroup과 capability를 함께 관리해 서비스 단위 최소 권한 구성을 돕는다.(freedesktop.org)

대표적인 옵션은 두 가지다.

  • CapabilityBoundingSet
    • 이 서비스가 가질 수 있는 capability의 상한을 정의
    • 나열되지 않은 capability는 설령 root여도 얻을 수 없다
  • AmbientCapabilities
    • 서비스 프로세스의 ambient 세트에 어떤 capability를 넣을지 정의
    • 비루트 계정으로 실행하면서 특정 권한만 부여할 때 적합

예를 들어 비루트 서비스에 포트 바인딩 권한만 주고 싶다면 다음과 같은 유닛을 작성할 수 있다.

[Service]
User=webuser
Group=webuser
ExecStart=/usr/local/bin/myweb
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

이렇게 하면 myweb 프로세스는 webuser 권한으로 실행되지만, CAP_NET_BIND_SERVICE capability를 보유하게 되어 80번 포트에 바인딩할 수 있다. 동시에 Bounding 세트가 이 capability 하나로 제한되므로, 다른 강력한 capability를 얻을 가능성도 차단된다.(Unix & Linux Stack Exchange)

이 방식의 장점은 바이너리를 교체하더라도 유닛 파일만 유지되면 capability 정책이 그대로 적용된다는 점이다. 배포 자동화와도 잘 어울린다.

설계와 운영 시 주의해야 할 보안 포인트

capability는 강력한 도구인 만큼 실수하면 위험도 크다. 최소 권한 설계를 할 때 특히 조심해야 할 부분을 정리해 보자.

첫째 CAP_SYS_ADMIN 같은 만능 capability를 쉽게 사용하지 않는다. 많은 보안 자료에서 이 capability 하나만으로도 사실상 root에 가까운 권한을 얻을 수 있다고 경고한다. 가능하면 더 좁은 범위의 capability 조합으로 대체하는 편이 좋다.(Man7)

둘째 스크립트 인터프리터에 직접 capability를 주는 것은 신중하게 검토한다. 예를 들어 python 바이너리에 cap_net_bind_service를 걸면 해당 파이썬으로 실행하는 모든 스크립트가 동일한 권한을 갖게 된다. 스크립트마다 권한을 달리 주고 싶다면 systemd 유닛이나 별도 래퍼 바이너리를 통해 제어하는 편이 안전하다.(Baeldung on Kotlin)

셋째 getcap로 정기 점검을 수행한다. 배포나 수동 작업 과정에서 의도치 않게 capability가 추가되거나 남아 있는 경우를 잡아낼 수 있다. 특히 침해 사고 조사나 보안 점검 시에는 루트 파일 시스템에 대해 getcap을 전체적으로 실행해 보는 것이 좋다.(Linux Audit)

넷째 capability를 통한 권한 상승 경로를 항상 염두에 둔다. 특정 capability를 가진 바이너리가 쉘을 실행하거나 임의 명령을 실행할 수 있다면, 사실상 그 capability 전체를 사용자에게 내어주는 효과가 된다. 기능을 분리하고 입력 검증을 강화해 악용 여지를 줄여야 한다.

마무리 정리

리눅스 capability는 더 이상 root 아니면 일반 사용자라는 단순한 모델에 머무르지 않고, 프로세스마다 필요한 만큼의 특권만 부여할 수 있게 해 주는 강력한 메커니즘이다.

파일 capability와 프로세스 capability 세트를 이해하고 setcap과 getcap으로 파일에 권한을 부여한 뒤, systemd의 CapabilityBoundingSet과 AmbientCapabilities 옵션을 함께 활용하면 서비스 단위 최소 권한 구성이 훨씬 수월해진다.

지금 운영 중인 서비스 가운데 아직도 습관적으로 root로 실행하는 데몬이 있다면, 그 서비스가 실제로 어떤 특권이 필요한지부터 목록을 뽑아 보자. 그리고 그 목록을 기반으로 capability 조합을 설계해 적용한다면, 같은 기능을 유지하면서도 훨씬 단단한 보안 토대를 갖춘 리눅스 환경을 만들 수 있다.