데몬은 chroot 실패를 기록하고 프로필은 이미 넉넉해 보이므로, 솔깃한 해법은 경로 규칙을 넓히고 재시작하는 것이다. 이것은 보통 아무것도 바꾸지 못하는데, chroot를 통제하는 것은 프로필이 애초에 넘겨줄 수 없었던 권한이기 때문이다. 어느 계층을 건드릴지 정하려면 값싼 확인 세 가지가 필요하다. 프로세스가 무엇을 보유할 수 있는지, 정책이 무엇을 허용하는지, 그리고 그 둘이 의미를 갖기 전에 실행기가 무엇을 제거했는지다.

두 개의 별개 관문, 그리고 AppArmor의 것은 하나뿐

chroot는 프로세스가 실행 중인 사용자 네임스페이스에 CAP_SYS_CHROOT을 요구한다. 이것은 관례적인 리눅스 권한이며, 호출이 경로로 무언가를 하기 전에 커널이 확인한다. 이와 별도로, 강제 모드의 AppArmor 프로필은 프로세스가 권한을 가지고 있어도 이를 거부할 수 있다. 필수 정책은 자격 증명 대신이 아니라 그에 더해 참조되기 때문이다. 이 구분은 실무에서 중요하다. 두 계층은 서로 다른 파일에 살고 보통 서로 다른 사람이 유지한다.

그 두 관문은 같은 errno로 실패한다. 둘 다 EPERM을 내고 둘 다 데몬 로그에 일반적인 권한 불만으로 나타나므로, 프로필 내용을 추측하는 데 그렇게 많은 시간이 낭비된다. 추가하려던 경로 규칙은 chroot가 성공한 뒤의 파일 접근을 통제한다. 그 작업이 애초에 허용되는지와는 아무 관련이 없다.

세 번째 관문은 둘 모두의 상류에 있다. 서비스 관리자나 래퍼가 프로세스를 그 경계 집합에 권한이 없는 상태로 시작했다면, 어떤 AppArmor 변경도 그것을 되돌릴 수 없고, 프로필에 권한을 더하는 것은 커널이 존중하지 않을 의도를 기록할 뿐이다.

정책을 편집하기 전에 권한 상태를 읽어라

활성이라고 믿는 구성이 아니라 실행 중인 프로세스에서 시작하라. 상태 파일은 다섯 개의 권한 마스크와 no-new-privileges 플래그를 드러내며, 그것들은 서로 다른 질문에 답한다.

grep -E '^(CapInh|CapPrm|CapEff|CapBnd|CapAmb|NoNewPrivs):' /proc/$(pidof openvpn)/status
sudo aa-status | grep -i openvpn

CapEff가 호출에 중요한 집합이다. CapBnd에는 있지만 CapEff에는 없는 권한은 상한이 허용하지만 현재 보유하지 않은 것이다. CapAmb는 바이너리가 통상적인 setuid 의미에서 특권이 아닐 때 중요하다. 앰비언트 권한이 서비스가 파일 권한 없이 exec를 넘어 특권을 유지하는 방법이기 때문이다. CapInh는 exec를 넘어 상속될 수 있는 비트를 담지만, 실행되는 파일의 상속 가능 집합이 허용할 때만이며, 그래서 거기의 비트 하나만으로는 아무것도 주지 않는다.

그다음 어떤 프로필이 실제로 붙어 있는지 확인하라. 프로세스는 예상하지 못한 프로필 이름으로 갇혀 있을 수 있고, 특히 배포판이 자체 유닛 파일과 프로필을 함께 보낼 때 그렇다. /proc/PID/attr/current를 읽으면 한 줄로 정리된다. 편집한 파일을 커널이 보고하는 프로필과 비교하는 것은 원인을 고치는 것과 아무도 읽지 않는 파일을 고치는 것의 차이다.

감사 흔적이 그렇게 말할 때만 권한 규칙을 더하라

커널과 감사 기록은 거부한 계층을 지목한다. 작업이 capable이고 이름이 sys_chroot인 거부를, 그것을 낳은 프로필과 함께 찾아라. 그 기록이 있으면 프로세스는 이미 권한을 가졌고 AppArmor가 거부한 것이므로, 겨냥할 변경은 실제 프로필 안의 권한 규칙이다.

capability sys_chroot,

그것을 진짜로 붙어 있는 프로필 안에 조각으로 두고, 키워드는 무관한 자습서에서 규칙을 베끼는 대신 apparmor.d 프로필 참조와 대조하라. 공유 include에 넣으면 그 파일을 끌어오는 모든 갇힌 프로그램에 부여된다. 프로필을 complain 모드로 바꾸면 이 문제를 해결하는 대신 다음 거부를 숨긴다. 프로필 규칙은 정책 거부를 없앨 뿐 특권을 만들어 내지 않으므로, 유효 집합에 CAP_SYS_CHROOT을 한 번도 가진 적 없는 프로세스는 규칙이 있어도 실패한다.

감사 기록에 AppArmor 거부가 전혀 없다면 프로필 편집을 멈춰라. 자격 증명 문제를 보고 있는 것이며, 남은 작업은 유닛 파일이나 부모 프로세스에 있지 정책 언어에 있지 않다.

서비스 관리자는 자기만의 상한을 유지한다

systemd는 서비스마다 권한 경계 집합을 설정하고, 기본값으로 그 집합은 좁다. 거기서 제외된 것은 프로필이 무엇이라 말하든 프로세스에 쓸 수 없으므로, 강화된 유닛은 데몬을 손으로 시작했을 때 동작하던 작업에서 실패할 수 있다. 관련 지시자는 유닛에서 읽기 쉽고, 애플리케이션이 실패를 보고할 때 놓치기도 쉽다. 배포판 패키지가 제공하는 drop-in이 아무도 설정했다고 기억하지 못하는 상한의 흔한 출처다.

상한을 올리는 것과 권한을 부여하는 것은 하나가 아니라 두 개의 변경이다. 경계 집합의 항목은 프로세스가 권한을 보유해도 된다고 말할 뿐, 유효 집합에 더하지 않는다. NoNewPrivs는 나머지가 모두 맞아도 나중의 승격이 성공하지 못하게 할 수 있다. 작업에 필요한 것만 더하고 나머지 강화는 그대로 두며, 호출 하나를 통과시키려고 모든 권한을 주는 지름길에 굴하지 마라.

여기서도 순서가 중요하다. 먼저 유닛의 상한을 바꾸고, CapEff에 이제 그 비트가 들어 있는지 확인한 다음에야 AppArmor가 여전히 거부하는지 결정하라. 두 변경을 한꺼번에 하면 실패가 계속될 때 다시 추측하게 된다.

거부가 사라진 뒤에는 감옥 자체를 확인하라

성공한 chroot는 루트를 옮기므로, 데몬이 그 뒤에 만지는 모든 경로는 새 트리 안에서 해석된다. 빠진 구성, 없는 장치 노드, 복사되지 않은 인증서 디렉터리는 이제 파일 오류로 나타나며, 그 오류는 권한 문제가 아니라 배치 문제다. 넓은 허가를 하나 더 넣는 대신 다음 이벤트를 읽어라. 이동은 상대 경로의 의미도 바꾸므로, 강화 안내서가 데몬 구성에서 절대 경로를 고집하는 것이다.

chroot가 하지 않는 일을 기억하는 것도 도움이 된다. 이것은 경로 경계이고 샌드박스가 아니다. 권한을 유지한 프로세스는 빠져나갈 수 있으며, 그래서 권한은 보통 호출 뒤에 떨어지고 격리는 데몬 주위에 남아 있어야 한다. AppArmor 프로필, 권한 경계 집합, 파일 시스템 제한을 함께 유지하는 것이 이 연습의 목적이며, 서비스가 시작되면 제거할 잔여물이 아니다.

결정 규칙은 짧다. 감사 기록이 AppArmor를 지목하면 붙어 있는 프로필에 권한 규칙을 하나 더하라. 유효 집합이 비어 있으면 유닛의 상한과 권한을 떨어뜨리는 순서를 고쳐라. 프로필이 편집한 것과 다르면 편집을 멈추고 진짜를 찾아라.

관련 글