13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] apparmor_parser

  • [Linux] AppArmor 프로파일 작성 및 디버깅: 리눅스 보안 실전 트러블슈팅

    [Linux] AppArmor 프로파일 작성 및 디버깅: 리눅스 보안 실전 트러블슈팅

    AppArmor 프로파일 작성 및 디버깅: 리눅스 보안 실전 트러블슈팅

    AppArmor 프로파일 작성은 문법 싸움보다 관찰과 축소의 싸움에 더 가깝습니다. 서비스가 멀쩡히 돌다가 배포 직후 파일 접근이 막히고, 로그에는 애매한 <code>DENIED만 남는 경우가 생각보다 자주 나오거든요. 저도 처음엔 규칙 몇 줄만 더하면 끝날 줄 알았는데, 실제 운영에선 /tmp, /run, 동적 라이브러리, 인증서 경로, systemd 런타임 디렉터리처럼 눈에 잘 안 보이는 지점에서 계속 걸리더라고요. 그래서 AppArmor 프로파일 작성은 “무엇을 허용할까”보다 먼저 “프로세스가 실제로 어디를 밟는가”를 확인하는 절차가 더 중요합니다.

    이번 글은 단순 사용법이 아니라, 실무에서 제가 기준으로 삼는 판단 순서를 중심으로 정리했습니다. 어떤 로그를 먼저 믿어야 하는지, 규칙을 어느 단위로 끊어야 나중에 안 무너지는지, AppArmor로 막는 게 맞는 문제와 구조를 먼저 손봐야 하는 문제를 어떻게 구분하는지까지 한 번에 다룹니다. 광고성 정보만 잔뜩 모은 글이 아니라, 읽고 바로 써먹을 수 있게 명령어와 프로파일 예시도 복붙 가능한 수준으로 넣었습니다.

    AppArmor 프로파일 작성 개요와 리눅스 AppArmor 적용 흐름 이미지

    AppArmor가 프로세스와 파일 경로를 기준으로 접근을 제어하는 흐름을 보여주는 개요 이미지입니다.

    AppArmor 프로파일 작성 전에 먼저 이해해야 할 것

    AppArmor는 경로 기반 MAC(Mandatory Access Control)입니다. 같은 리눅스 보안 계층이라도 SELinux처럼 라벨을 설계하는 방식과는 결이 조금 다릅니다. AppArmor 쪽이 초반 진입은 쉬운 편이지만, 반대로 경로가 지저분한 애플리케이션에서는 정책도 금방 지저분해집니다. 그래서 저는 AppArmor를 잘 쓰는 조건을 이렇게 봅니다.

    • 데이터 경로가 분리돼 있다. 예: /var/lib/myapp, /etc/myapp, /run/myapp
    • 외부 프로세스 실행이 적다.
    • 임시 파일, 소켓, PID 파일 위치를 예측할 수 있다.
    • systemd 유닛이 비교적 단순하다.

    반대로 애플리케이션이 실행 중 여기저기 흩어진 경로를 건드리고, 셸을 여러 번 호출하고, 플러그인이나 후처리 스크립트를 동적으로 실행한다면 프로파일 작성보다 애플리케이션의 파일 구조를 먼저 정리하는 편이 비용 대비 효과가 큽니다. 이걸 무시하고 규칙만 늘리면 결국 /var/** rw, 같은 과한 허용으로 흘러가고, 그 시점부터는 보호 가치가 급격히 떨어집니다.

    Enforce와 Complain을 나누는 기준

    • enforce: 정책 위반을 차단합니다. 운영 보호용입니다.
    • complain: 일반 허용 규칙 밖 접근을 차단하지 않고 로그만 남깁니다. 다만 프로파일에 명시한 deny 규칙은 complain 모드에서도 계속 적용됩니다.

    여기서 많이 하는 실수가 “테스트 서버니까 바로 enforce”입니다. 저는 오히려 반대로 갑니다. 테스트 환경일수록 동작 패턴이 덜 안정적이라 예외 경로가 많고, 그래서 처음엔 complain이 낫습니다. enforce는 “정상 경로와 실패 경로를 둘 다 재현했고, 새 deny 로그가 더 이상 의미 있게 안 나온다”는 조건이 붙을 때 올립니다. 운영에서 장애 한 번 나면 AppArmor 자체에 대한 조직 신뢰가 확 꺾이거든요.

    리눅스 AppArmor 운영 전에 알아둘 파일과 도구

    리눅스 AppArmor를 만질 때는 도구를 많이 아는 것보다 언제 무엇을 쓰는지가 더 중요합니다. 아래 정도만 먼저 손에 익혀도 실무 대응 속도가 꽤 빨라집니다.

    • /etc/apparmor.d/: 프로파일 저장 위치
    • aa-status: 로드된 프로파일, enforce/complain 상태 확인
    • aa-complain, aa-enforce: 프로파일 모드 전환
    • apparmor_parser: 문법 검사와 프로파일 재로드
    • journalctl -k, dmesg, /var/log/audit/audit.log: 커널 또는 audit 로그 확인
    • aa-logprof: 로그 기반 규칙 제안
    • systemctl cat: systemd 유닛 정의 확인

    현장에서 바로 보는 기본 점검은 보통 이 정도면 충분합니다.

    sudo systemctl status apparmor
    sudo aa-status
    sudo ls /etc/apparmor.d/
    sudo journalctl -k -g apparmor -b
    

    제가 중요하게 보는 건 숫자보다 맥락입니다. aa-status에서 특정 프로파일이 로드됐는지, 어떤 모드인지 먼저 확인하고, journalctl -k -g apparmor -b로는 현재 부팅에서 실제 deny가 있었는지를 봅니다. auditd를 쓰는 서버라면 로그가 /var/log/audit/audit.log 쪽에 남을 수 있으니 그 점도 같이 확인해두면 덜 헤맵니다.

    AppArmor 프로파일 작성 실전: 작은 서비스부터 잠그는 게 맞습니다

    예시 시나리오를 하나 고정해보겠습니다. /usr/local/bin/report-sync라는 내부 동기화 스크립트가 있고, 설정은 /etc/report-sync/config.yml, 데이터는 /var/lib/report-sync/, 실행 중 락 파일은 /run/report-sync/에 남긴다고 가정하겠습니다. 외부 API 호출은 필요하지만 셸 실행은 필요 없다고 두겠습니다. 이 정도로 경로를 정리해두면 AppArmor 프로파일 작성 난도가 확 내려갑니다.

    1. 프로세스가 밟아야 하는 경로를 먼저 적습니다.
    2. 초안은 짧게 씁니다. 처음부터 다 맞히려 하지 않습니다.
    3. complain 모드에서 정상 시나리오와 실패 시나리오를 둘 다 돌립니다.
    4. deny 로그를 읽고 규칙을 추가하되, 상위 디렉터리를 통째로 열지 않습니다.
    5. 새 deny가 사라진 뒤 enforce로 올립니다.

    1. 프로파일 초안 작성

    sudo tee /etc/apparmor.d/usr.local.bin.report-sync > /dev/null <<'EOF'
    #include <tunables/global>
    
    /usr/local/bin/report-sync {
      #include <abstractions/base>
      #include <abstractions/nameservice>
    
      /usr/local/bin/report-sync r,
      /etc/report-sync/config.yml r,
    
      /var/lib/report-sync/ r,
      /var/lib/report-sync/** rwk,
    
      /run/report-sync/ rw,
      /run/report-sync/** rwk,
    
      network inet tcp,
      network inet6 tcp,
    
      deny /bin/sh x,
      deny /usr/bin/bash x,
      deny /usr/bin/dash x,
    }
    EOF
    

    여기서 몇 가지를 짚고 넘어가야 합니다. r은 읽기, w는 쓰기, k는 파일 잠금(lock), x는 실행입니다. 락 파일이나 PID 파일을 다루는 프로그램은 w만으로 부족할 때가 꽤 있습니다. 실제로 flock 같은 잠금 동작이 들어가면 k가 빠져서 계속 막히는 사례를 자주 봤습니다. 또 디렉터리 자체 권한과 하위 파일 권한은 분리해서 봐야 합니다. /var/lib/report-sync/ r,와 /var/lib/report-sync/** rwk,를 나눠 적는 이유도 그 지점 때문입니다.

    네트워크도 무조건 한 줄로 끝나진 않습니다. 단순 TCP 클라이언트라면 위 예시 정도로 시작할 수 있지만, 유닉스 도메인 소켓이나 raw 소켓을 쓰는 프로그램은 다른 규칙이 필요할 수 있습니다. 그래서 초안은 어디까지나 학습용 가설로 받아들이는 게 맞습니다.

    AppArmor 프로파일 작성 예시와 파일 경로 매핑 이미지

    프로파일 파일, 바이너리 경로, 허용된 데이터 디렉터리의 관계를 보여주는 구성 이미지입니다.

    2. 문법 검사와 complain 전환

    sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.report-sync
    sudo aa-complain /usr/local/bin/report-sync
    sudo aa-status | grep report-sync
    

    이 단계에서는 두 부류의 실패를 분리해서 봐야 합니다. 파서 오류는 프로파일 문법 문제고, 실행 중 deny는 권한 설계 문제입니다. 둘을 섞어 보면 디버깅 시간이 괜히 길어집니다. 파서가 깨질 때는 보통 중괄호 위치, 규칙 끝의 쉼표 누락, include 경로, 권한 토큰 오타 같은 기본 문법이 원인입니다.

    3. 실제 실행과 로그 수집

    sudo /usr/local/bin/report-sync
    sudo journalctl -k -g apparmor --since "10 minutes ago"
    sudo dmesg | grep -i apparmor
    

    로그를 볼 때는 한 줄 전체를 멍하니 읽기보다 핵심 필드만 먼저 잡는 편이 훨씬 빠릅니다.

    • operation=: 무엇을 하려 했는가. 예: open, exec, connect
    • profile=: 어떤 프로파일에 걸렸는가
    • name=: 실제 대상 경로는 무엇인가
    • requested_mask=: 프로세스가 원한 권한은 무엇인가
    • denied_mask=: 실제 막힌 권한은 무엇인가

    예를 들어 operation="open", name="/run/report-sync/lock", denied_mask="k"가 찍히면 이건 단순 쓰기 누락이 아니라 잠금 권한이 빠진 겁니다. 또 operation="exec"가 보이면 파일 접근 문제로만 보면 안 됩니다. 애플리케이션이 내부에서 다른 바이너리를 호출하고 있다는 뜻이라, 그 시점부터는 실행 전이인 ix, px, Px 같은 규칙을 검토해야 합니다.

    트러블슈팅: 실제 운영에서 자주 막히는 지점과 근본 원인

    AppArmor 디버깅은 규칙 추가 게임이 아닙니다. 왜 그 접근이 필요한지 이해하지 못한 채 허용만 늘리면, 나중에 문제를 재현하기가 더 어려워집니다. 아래는 제가 반복해서 본 실패 패턴입니다.

    문제 1. 설정 파일은 열었는데, 여전히 특정 파일 접근이 실패하는 경우

    겉으로는 /etc/myapp/config.yml만 읽는 것처럼 보여도 실제론 CA 인증서, locale 파일, 동적 모듈, 캐시 디렉터리, 소켓 파일을 추가로 건드리는 경우가 많습니다. 특히 HTTPS 통신을 하는 CLI는 /etc/ssl/ 계열 경로를 간접적으로 읽을 수 있습니다. 이럴 때 상위 디렉터리를 크게 열어버리면 빠르긴 해도, 나중에 왜 허용됐는지 설명이 안 됩니다.

    sudo aa-logprof
    

    aa-logprof는 유용하지만, 저는 제안된 규칙을 그대로 다 받지 않습니다. 기준은 단순합니다. 예측 가능한 고정 경로인가, 실패 원인이 해당 경로 하나로 닫히는가, 와일드카드를 쓰지 않아도 되는가를 먼저 봅니다. 예를 들어 설정 파일 1개 때문에 /etc/** r,를 받아들이면 그 순간 관리가 무너집니다.

    문제 2. 수동 실행은 되는데 systemd 서비스로만 실패하는 경우

    이건 정말 흔합니다. 원인은 보통 AppArmor 자체보다 실행 컨텍스트 차이입니다. systemd는 사용자, 작업 디렉터리, 환경 변수, RuntimeDirectory, StateDirectory, 소켓 활성화 여부가 전부 다를 수 있습니다. 수동으로는 안 밟던 /run/... 경로가 서비스 모드에선 갑자기 생기는 이유도 여기 있습니다.

    sudo systemctl cat report-sync.service
    sudo systemctl show report-sync.service -p User -p Group -p RuntimeDirectory -p StateDirectory -p WorkingDirectory
    sudo journalctl -u report-sync.service -b
    

    제가 보는 포인트는 세 가지입니다. 누가 실행하는가, 런타임 파일이 어디에 생기는가, ExecStartPre/ExecStartPost에서 별도 바이너리를 호출하는가입니다. 특히 전처리 스크립트가 있으면 메인 바이너리만 프로파일링해서는 잘 안 끝나더라고요.

    문제 3. deny 로그가 없는데 서비스가 죽는 경우

    이럴 땐 AppArmor만 붙들고 있으면 시간 낭비입니다. deny가 없으면 실제 차단은 없었다는 뜻일 가능성이 높습니다. 애플리케이션 자체 예외, 파일 소유권 문제, 잘못된 경로, systemd 타임아웃, 의존 서비스 미기동 같은 다른 층위를 바로 봐야 합니다.

    상황 우선 확인할 것 근본 원인 후보 제가 권하는 대응
    커널 또는 audit 로그에 AppArmor deny 존재 journalctl -k -g apparmor, /var/log/audit/audit.log 프로파일이 실제 접근을 차단 경로와 권한을 최소 범위로 추가
    deny 없음, 앱 로그에 파일 오류만 존재 애플리케이션 로그와 파일 소유권 경로 오타, 파일 미생성, UNIX 권한 문제 AppArmor보다 앱 설정과 POSIX 권한부터 확인
    수동 실행 성공, systemd 실패 systemctl show, /run, 실행 계정 실행 컨텍스트 차이 런타임 디렉터리와 보조 프로세스 경로를 반영
    규칙이 과도하게 많아짐 프로파일 범위와 파일 구조 애플리케이션 경로 설계가 산만함 정책 추가보다 경로 재구성이나 서비스 분리 검토
    operation="exec" deny 반복 호출되는 하위 바이너리 목록 외부 명령 실행 설계 무분별한 x 허용 대신 실행 전이 정책 검토

    이 표에서 핵심은 “어느 계층부터 볼 것인가”입니다. deny가 있으면 AppArmor부터, deny가 없으면 다른 원인부터 보는 식으로 순서를 고정해두면 추측이 크게 줄어듭니다.

    리눅스 AppArmor deny 로그 분석과 트러블슈팅 이미지

    커널 로그에서 operation, profile, name, denied_mask를 읽는 포인트를 강조한 이미지입니다.

    AppArmor 프로파일 작성 규칙을 다듬는 기준: 파일이 아니라 동작으로 자르세요

    제가 AppArmor 프로파일 작성에서 가장 경계하는 건 “일단 넓게 열고 나중에 줄이자”는 접근입니다. 실제로는 나중에 거의 안 줄어듭니다. 운영에 들어간 허용 규칙은 잘 안 걷히거든요. 그래서 저는 파일 기준보다 동작 기준으로 규칙을 끊습니다.

    • 설정 읽기: /etc/myapp/*.conf r,
    • 상태 저장: /var/lib/myapp/** rwk,
    • 로그 쓰기: /var/log/myapp/** w,
    • 런타임 파일: /run/myapp/** rwk,
    • DNS 조회 필요: #include <abstractions/nameservice>
    • 외부 프로세스 실행 필요: 대상 바이너리별로 별도 검토

    여기서 실행 권한은 특히 조심해야 합니다. x 하나로 끝나는 문제가 아니고, ix, px, Px처럼 실행 시 현재 프로파일을 상속할지, 별도 프로파일로 전환할지에 따라 보안 성질이 달라집니다. 저는 내부 배치가 tar, gzip, rsync 같은 외부 명령을 꼭 호출해야 할 때도 무작정 허용하지 않고, “정말 이 호출이 애플리케이션 내부에 있어야 하나”를 먼저 봅니다. 이거 한번 습관 붙여두면 정책이 훨씬 덜 지저분해집니다.

    언제 AppArmor를 쓰고, 언제 구조를 먼저 손봐야 하는가

    이 질문이 실무에선 더 중요합니다. 모든 서비스에 AppArmor를 똑같이 적용하는 건 좋은 운영이라고 보기 어렵습니다.

    상황 AppArmor 우선 구조 정리 우선
    내부 배치 스크립트, 작은 데몬 권장 보통 불필요
    데이터 경로가 명확한 API 서비스 권장 필요 시 병행
    여러 외부 명령을 연쇄 호출하는 모놀리식 앱 신중 우선 검토
    임시 파일과 소켓 위치가 제각각인 레거시 앱 신중 우선 검토
    서드파티 바이너리 보호 효과 큼 수정 불가라면 AppArmor 쪽이 현실적

    제가 실제로 추천하는 방향은 분명합니다. 작고 경로가 단순한 서비스는 AppArmor를 바로 적용하시고, 경로와 실행 흐름이 복잡한 서비스는 규칙을 늘리기 전에 파일 구조와 런타임 경로를 정리하시는 편이 낫습니다. AppArmor는 엉킨 구조를 마법처럼 정리해주는 도구라기보다, 정리된 구조를 더 안전하게 잠가주는 도구에 가깝습니다.

    검증: complain에서 enforce로 넘기기 전에 꼭 보는 체크리스트

    서비스가 한 번 성공했다고 바로 enforce로 올리면, 야간 배치나 장애 복구 루틴에서 다시 터질 수 있습니다. 그래서 저는 최소한 아래 단계를 거칩니다.

    1. 정상 시나리오를 여러 번 실행합니다.
    2. 재시도, 파일 없음, 로그 회전 직후처럼 실패 시나리오도 일부러 만듭니다.
    3. journalctl -k -g apparmor와 필요 시 audit 로그에 새 deny가 없는지 확인합니다.
    4. 프로파일을 다시 읽고, 목적이 모호한 와일드카드를 줄입니다.
    5. 그 다음에 enforce로 전환합니다.
    sudo aa-enforce /usr/local/bin/report-sync
    sudo aa-status | grep report-sync
    sudo journalctl -k -g apparmor --since "5 minutes ago"
    

    이때 기준은 단순합니다. 기능 성공 + 새 deny 없음 + 규칙 목적이 설명 가능함. 이 세 가지가 맞아야 합니다. deny가 계속 찍히는데 기능이 우연히 통과한 상태라면 아직 끝난 게 아닙니다. 선택 경로나 예외 처리 루틴에서 나중에 터질 가능성이 남아 있습니다.

    AppArmor 프로파일 작성에서 complain 모드와 enforce 모드 비교 이미지

    학습 단계인 complain 모드와 차단 단계인 enforce 모드의 차이를 한눈에 비교하는 이미지입니다.

    제가 많이 헤맸던 함정 하나: /tmp를 열어도 안 풀리는 이유

    예전에 백업 후처리 스크립트를 묶을 때 로그에는 /tmp/backup.lock만 보였습니다. 그래서 /tmp/** rwk,를 추가했는데도 실패가 계속됐습니다. 원인은 다른 데 있었습니다. 서비스가 systemd의 notify 메커니즘을 통해 상태를 알리고 있었고, 실제로는 /run/systemd/notify 계열 소켓 접근이 필요했던 겁니다. 겉으로 드러난 첫 번째 경로만 보고 달려들면 이런 식으로 시간을 잃습니다. 이거 한번 겪고 나면 로그 한 줄만 믿으면 안 된다는 걸 몸으로 배우게 됩니다.

    이 경험 이후로는 deny 로그를 한 줄만 보지 않습니다. 서비스가 어떤 타입으로 실행되는지, PID 파일과 소켓을 어디에 남기는지, 보조 바이너리를 호출하는지를 같이 봅니다. AppArmor 디버깅은 결국 프로세스의 발자국을 복원하는 작업입니다. 그 발자국이 정리되면 규칙은 오히려 짧아집니다.

    자주 묻는 질문

    프로파일은 수동으로 쓰는 게 좋을까요, 도구를 써야 할까요?

    초안은 수동으로 짧게 잡고, 실행 후엔 aa-logprof로 보완하는 방식을 권합니다. 처음부터 도구 제안을 전부 수락하면 과도한 허용이 들어가기 쉽습니다.

    requested_mask와 denied_mask는 어떻게 읽어야 하나요?

    requested_mask는 프로세스가 시도한 권한이고, denied_mask는 그중 실제 막힌 권한입니다. 읽기만 필요한 줄 알았는데 쓰기나 잠금까지 요청한다면, 허용을 늘리기 전에 애플리케이션 동작 자체를 다시 보는 게 맞습니다.

    홈랩과 운영 서버는 접근 방식을 달리해야 할까요?

    그렇습니다. 홈랩은 complain으로 동작 패턴을 학습하기 좋고, 운영은 이미 검증된 프로파일만 enforce로 올리는 편이 안정적입니다. 저는 홈랩에서 deny 패턴을 먼저 모아두고 운영에선 그 결과만 가져가는 방식을 선호합니다.

    실무에서 바로 적용할 추천안

    AppArmor 프로파일 작성이 필요한 상황이라면, 먼저 서비스의 설정 경로, 상태 저장 경로, 런타임 경로를 분리해두세요. 그다음 짧은 프로파일로 시작해서 complain에서 로그를 수집하고, deny를 보고 최소 범위로만 열어주면 됩니다. 이 순서를 지키면 정책이 쓸데없이 커지는 걸 꽤 잘 막을 수 있습니다.

    판단 기준도 분명하게 가져가시면 됩니다. 내부 스크립트, 배치, 작은 데몬, 서드파티 바이너리는 AppArmor 적용 효과가 큽니다. 반면 외부 명령 실행이 많고 경로가 산만한 레거시 서비스는 규칙을 늘리기 전에 구조를 먼저 정리하세요. 관련해서 SELinux 비교 글이나 systemd 보안 옵션 정리 글도 내부 링크로 함께 묶어두면 독자 체류 시간이 꽤 좋아집니다.