13년차의 서버실

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

[태그:] AppArmor

  • [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 보안 옵션 정리 글도 내부 링크로 함께 묶어두면 독자 체류 시간이 꽤 좋아집니다.

  • [보안] 리눅스 서버 하드닝 전략 재점검 체크리스트

    [보안] 리눅스 서버 하드닝 전략 재점검 체크리스트

    리눅스 서버 하드닝 전략 재점검 체크리스트

    요즘 보안 메일함 열어보면 심장이 조금 빨리 뛰죠. 특히 리눅스 서버 하드닝 관점에서 보면, 커널과 사용자 공간(User space, 커널 밖에서 도는 일반 프로세스) 취약점 공지가 꽤 자주 올라옵니다. 저도 홈랩이랑 운영 서버를 같이 굴리다 보니, 처음엔 “요즘 왜 이렇게 Linux CVE가 많아졌지?” 싶었거든요. 그런데 실제로 뜯어보니, 단순히 숫자만 늘었다기보다 공개 체계가 더 촘촘해진 부분, 그리고 실제로 빨리 패치해야 하는 취약점이 섞여 있더라고요.

    특히 2024년 2월 13일에 kernel.org가 CVE Numbering Authority(CNA, CVE 번호 발급 권한 기관)로 추가된 뒤부터는 Linux 커널 취약점에 CVE가 더 체계적으로 붙기 시작했습니다. 거기에 CISA가 실제 악용 사례가 있는 Linux kernel 취약점들을 Known Exploited Vulnerabilities(KEV) 목록에 올리면서, “아, 이건 숫자 구경만 할 게 아니구나” 싶었거든요. 리눅스 보안은 결국 패치 속도만의 문제가 아니라, 노출면(Attack Surface, 공격 가능 면적)을 줄이는 운영 습관의 문제이기도 합니다.

    리눅스 서버 하드닝 전체 구조를 보여주는 아키텍처 이미지

    커널, SSH, 방화벽, MAC 정책, 패치 자동화, 검증 흐름까지 한눈에 보여주는 개요 이미지입니다.

    1. 최근 Linux CVE가 많이 보이는 이유부터 정리해보겠습니다

    쉽게 말해, 요즘 보이는 “급증”은 두 가지가 섞여 있습니다. 발견과 공개가 더 적극적이 된 점, 그리고 실제 운영자가 체감할 정도로 대응 압박이 커진 점이 같이 온 겁니다.

    구분 무슨 뜻인가 운영자가 봐야 할 포인트
    CVE 공개 체계 변화 kernel.org가 2024-02-13부터 CNA 역할 수행 이전보다 커널 취약점이 더 잘 드러날 수 있음
    실제 악용 사례 CISA가 Linux kernel CVE를 KEV에 추가 패치 우선순위를 높게 잡아야 함
    자동화된 버그 탐지 fuzzing과 정적/동적 분석이 더 활발 작은 버그도 더 빨리 공론화됨

    여기서 중요한 포인트가 있습니다. CVE 숫자가 늘었다 = 리눅스가 갑자기 위험해졌다로 바로 연결하면 좀 단순합니다. 제가 직접 홈랩 커널 업데이트 흐름을 따라가 보니까, 예전엔 배포판 공지 뒤에 숨어 지나가던 수정이 이제는 CVE로 더 명확하게 드러나는 경우도 꽤 있더라고요. 반대로, KEV에 들어간 건 진짜 우선순위가 다릅니다. 이건 “나중에 점검”이 아니라 즉시 확인 쪽입니다.

    2. 리눅스 서버 하드닝 체크리스트, 우선순위는 이렇게 잡으시면 됩니다

    저는 보통 패치, 접근 통제, 권한 축소, 관측성 순으로 봅니다. 이유는 간단하거든요. 멋진 보안 솔루션보다 먼저, 뚫릴 구멍을 줄이는 게 훨씬 싸고 빠릅니다.

    1. 커널과 패키지 업데이트 기준선 만들기: 지금 어떤 커널을 쓰는지, 보안 업데이트가 몇 건 밀렸는지 숫자로 확인합니다.
    2. SSH 노출면 줄이기: 비밀번호 로그인, root 직접 로그인, 불필요한 포트 노출을 먼저 줄입니다.
    3. 방화벽 기본 정책 정리: 허용 목록(Allowlist, 허용 대상만 열기) 방식으로 바꿉니다.
    4. SELinux/AppArmor 같은 MAC 적용: 뚫려도 옆으로 번지는 걸 막습니다.
    5. systemd sandboxing: 서비스 단위로 권한을 더 잘게 자릅니다.
    6. 감사 로그와 검증 루틴: 적용 후 확인하지 않으면 하드닝이 아니라 기분만 좋아지는 설정이 됩니다.

    3. 실전 구현 1단계: 패치 상태와 커널 노출 범위부터 확인합니다

    처음엔 이게 뭔가 싶었는데, 결국 출발점은 단순합니다. 무엇이 돌아가고 있는지 알아야 CVE 대응도 빠릅니다. 아래 명령은 제가 새 서버 잡으면 거의 습관처럼 먼저 칩니다.

    uname -r
    cat /etc/os-release
    ss -tulpn
    systemctl --failed
    

    uname -r은 현재 커널 버전, ss -tulpn은 외부에 열려 있는 포트와 프로세스를 같이 보여줍니다. 여기서 예상 밖 포트가 보이면, 그 서버는 이미 서버 보안 강화 대상입니다.

    Debian/Ubuntu 계열이면 보안 업데이트 확인은 이렇게 시작하시면 됩니다.

    sudo apt update
    apt list --upgradable
    sudo unattended-upgrades --dry-run -d
    

    RHEL 계열은 보통 이렇게 봅니다.

    sudo dnf check-update
    sudo dnf updateinfo list security
    

    Ubuntu를 쓰신다면 Livepatch(재부팅 없이 일부 커널 보안 수정 적용)도 검토할 만합니다. 다만 Canonical 문서 기준으로 Livepatch만 믿고 장기간 재부팅을 미루면 안 됩니다. 지원되는 커널 ABI 범위와 재부팅 주기가 따로 있거든요. 실제로 써보니까 편하기는 한데, 이것도 결국 정식 커널 교체를 완벽히 대체할 수는 없더라고요.

    4. 실전 구현 2단계: SSH와 네트워크부터 조입니다

    제가 제일 먼저 손대는 부분입니다. CVE가 터졌을 때 제일 아쉬운 서버가 어떤 서버냐 하면, 패치가 늦은 서버보다도 불필요하게 다 열어둔 서버더라고요. 삽질 좀 했습니다 ㅎㅎ

    sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)
    sudoedit /etc/ssh/sshd_config
    
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PubkeyAuthentication yes
    MaxAuthTries 3
    AllowUsers adminops
    

    적용 전에는 반드시 문법 검사를 하세요.

    sudo sshd -t && sudo systemctl reload sshd
    

    방화벽은 nftables 기준으로 아주 단순하게 시작하는 편이 좋습니다.

    sudo mkdir -p /etc/nftables
    sudoedit /etc/nftables.conf
    
    #!/usr/sbin/nft -f
    flush ruleset
    
    table inet filter {
      chain input {
        type filter hook input priority 0;
        policy drop;
        iif lo accept
        ct state established,related accept
        tcp dport { 22, 80, 443 } accept
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept
      }
    
      chain forward {
        type filter hook forward priority 0;
        policy drop;
      }
    
      chain output {
        type filter hook output priority 0;
        policy accept;
      }
    }
    
    sudo nft -f /etc/nftables.conf
    sudo nft list ruleset
    sudo systemctl enable --now nftables
    
    리눅스 서버 하드닝에서 SSH와 방화벽 정책을 설명하는 이미지

    SSH 접근 제한, 허용 포트 최소화, 상태 기반 방화벽 정책을 시각적으로 보여주는 이미지입니다.

    5. 실전 구현 3단계: SELinux, AppArmor, systemd sandboxing으로 한 번 더 막습니다

    Mandatory Access Control(MAC, 강제 접근 통제)은 초반엔 귀찮아 보여도, 사고 났을 때 진짜 값어치를 합니다. Red Hat 문서도 SELinux를 추가 보안 계층으로 설명하고 있고, Ubuntu 문서도 AppArmor를 경로 기반 MAC으로 안내합니다. 쉽게 말해 프로세스가 원래 해야 할 일만 하게 만드는 장치입니다.

    RHEL 계열에서는 SELinux 상태를 먼저 확인하세요.

    getenforce
    sestatus
    sudo setenforce 1
    

    Ubuntu 계열에서는 AppArmor 상태를 확인합니다.

    sudo apparmor_status
    sudo aa-enforce /etc/apparmor.d/*
    

    그리고 요즘 제가 자주 같이 보는 게 systemd-analyze security입니다. 서비스별 노출 점수를 빠르게 볼 수 있어서, 감으로 하드닝하는 느낌이 훨씬 줄어들더라고요.

    sudo systemd-analyze security nginx.service
    

    서비스 오버라이드(override)로 샌드박싱을 추가하는 예시는 아래처럼 시작하시면 됩니다.

    sudo systemctl edit nginx.service
    
    [Service]
    NoNewPrivileges=yes
    PrivateTmp=yes
    ProtectSystem=strict
    ProtectHome=yes
    PrivateDevices=yes
    RestrictSUIDSGID=yes
    LockPersonality=yes
    MemoryDenyWriteExecute=yes
    RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
    SystemCallArchitectures=native
    
    sudo systemctl daemon-reload
    sudo systemctl restart nginx
    sudo systemd-analyze security nginx.service
    

    이거 실제로 써보니까 꽤 좋더라고요. 다만 여기서 중요한 건, 서비스별로 필요한 권한이 다르다는 점입니다. DB나 백업 에이전트는 너무 세게 조이면 바로 깨집니다.

    6. 커널 노출면 줄이는 sysctl과 감사 로그도 같이 가야 합니다

    리눅스 서버 하드닝에서 빠지기 쉬운 게 커널 파라미터와 로그입니다. 공격 자체를 막는 것도 중요하지만, 이상 징후를 빨리 보는 것도 중요하거든요.

    sudoedit /etc/sysctl.d/99-hardening.conf
    
    net.ipv4.conf.all.accept_redirects = 0
    net.ipv4.conf.default.accept_redirects = 0
    net.ipv4.conf.all.send_redirects = 0
    net.ipv4.conf.default.send_redirects = 0
    net.ipv4.conf.all.rp_filter = 1
    net.ipv4.conf.default.rp_filter = 1
    net.ipv4.tcp_syncookies = 1
    kernel.kptr_restrict = 2
    kernel.dmesg_restrict = 1
    fs.protected_symlinks = 1
    fs.protected_hardlinks = 1
    
    sudo sysctl --system
    

    감사 로그(audit log)도 최소한은 잡아두는 편이 좋습니다.

    sudo apt install auditd -y || sudo dnf install audit -y
    sudo systemctl enable --now auditd
    
    sudo auditctl -w /etc/ssh/sshd_config -p wa -k ssh_config_change
    sudo auditctl -w /etc/sudoers -p wa -k sudoers_change
    sudo auditctl -w /usr/bin/sudo -p x -k sudo_exec
    

    여기서 정말 중요한 포인트예요. 로그는 쌓는 것보다 어떤 이벤트를 보면 위험 신호로 판단할지가 더 중요합니다. SSH 설정 변경, sudo 실행 급증, 예상하지 못한 서비스 재시작 같은 건 바로 알아채야 하거든요.

    리눅스 서버 하드닝에서 SELinux AppArmor 시스템 샌드박싱을 보여주는 이미지

    프로세스 권한 최소화와 서비스 격리를 여러 층으로 적용하는 장면을 표현한 이미지입니다.

    7. ⚠️ 실제로 자주 겪는 문제와 트러블슈팅

    하드닝은 설정 넣는 순간보다, 그 뒤에 안 깨지게 만드는 과정이 더 어렵습니다. 저도 처음엔 자신 있게 적용했다가 웹 서비스가 파일 못 읽어서 502 띄운 적 있습니다.

    • SELinux/AppArmor 적용 후 서비스 장애: 먼저 비활성화하지 말고 로그를 봅니다. SELinux는 /var/log/audit/audit.log, AppArmor는 journalctl -xe나 /var/log/syslog 쪽부터 확인하세요.
    • systemd sandboxing 후 서비스 기동 실패: ProtectSystem=strict, ProtectHome=yes가 자주 원인입니다. 서비스가 실제로 써야 하는 경로를 ReadWritePaths=로 열어줘야 할 수 있습니다.
    • SSH 설정 변경 후 원격 접속 끊김: 운영 중인 세션을 유지한 상태에서 새 세션으로 테스트하고, 가능하면 콘솔 접근 수단을 남겨두세요.
    • 방화벽 적용 후 내부 통신 장애: 모니터링, 백업, 노드 간 헬스체크 포트를 빼먹는 경우가 많습니다. 홈랩에서 Kubernetes 올릴 때 저도 이걸로 반나절 썼습니다.

    그래서 저는 항상 한 번에 다 바꾸지 않고, 서비스 하나씩 묶어서 적용합니다. 보안 체크리스트는 체크만 하면 끝나는 문서가 아니라, 배포 순서를 정리하는 운영 문서여야 하더라고요.

    8. 검증, 결과 확인, 그리고 다음 단계

    설정이 들어갔으면 반드시 검증해야 합니다. 아래 정도는 최소 루틴으로 가져가면 좋습니다.

    1. 커널/패키지 버전 확인: 패치 적용 전후 버전 차이를 기록합니다.
    2. 포트 노출 재검사: ss -tulpn 결과를 저장해 비교합니다.
    3. 서비스 샌드박스 점검: systemd-analyze security로 전후 점수를 비교합니다.
    4. 로그 확인: SELinux/AppArmor 거부 로그, audit 로그, 인증 로그를 같이 봅니다.
    5. 재부팅 리허설: 커널 업데이트 후 서비스가 정상 복구되는지 꼭 확인합니다.
    uname -r
    ss -tulpn
    sudo systemd-analyze security
    sudo journalctl -p warning -b
    sudo ausearch -k ssh_config_change
    

    제가 직접 해보니 가장 효과가 큰 건 화려한 솔루션이 아니라 패치 기준선 + SSH 제한 + 방화벽 기본 거부 + MAC + 서비스 샌드박스 이 5개였습니다. 드디어 됐다! 싶은 순간이 오긴 오는데, 사실 그 뒤가 더 중요합니다. 운영 환경은 계속 바뀌니까요.

    리눅스 서버 하드닝 적용 전후 결과를 보여주는 검증 이미지

    적용 전후 비교 결과를 대시보드 형태로 보여주는 검증 이미지입니다.

    정리: 지금 바로 다시 볼 체크포인트

    • 리눅스 서버 하드닝은 CVE 개수보다 노출면 축소가 핵심입니다.
    • kernel.org CNA 전환과 실제 악용 사례 공개를 보면, 공개량 증가와 실제 위험 신호를 구분해서 봐야 합니다.
    • CVE 대응은 패치만이 아니라 SSH, nftables, SELinux/AppArmor, systemd sandboxing까지 묶어서 봐야 합니다.
    • 검증 없는 하드닝은 절반짜리입니다. 적용 후 로그와 서비스 상태를 꼭 확인하세요.

    다음 글에서는 systemd 서비스별 하드닝 템플릿을 좀 더 깊게 다뤄볼 예정입니다. 이전 글에서 다뤘던 로그 수집 파이프라인이 있으시면 그쪽과 묶어서 보셔도 좋고요.

    패치, 접근 통제, MAC, 샌드박스, 로그 검증을 우선순위별로 정리한 요약 이미지입니다.

    참고한 공개 자료

  • [Linux 보안] SELinux vs AppArmor: 선택 가이드와 실전 비교

    [Linux 보안] SELinux vs AppArmor: 선택 가이드와 실전 비교

    [Linux 보안] SELinux vs AppArmor: 선택 가이드와 실전 비교

    리눅스 서버를 운영하다 보면 결국 한 번은 붙잡게 되는 주제가 있습니다. 바로 SELinux AppArmor 비교입니다. 방화벽만 잘 열고 닫는다고 끝나는 게 아니더라고요. 실제 운영 환경에서는 프로세스가 어디까지 접근할 수 있는지, 침해 사고가 났을 때 피해 범위를 얼마나 좁힐 수 있는지가 정말 중요합니다. 저도 홈랩에서 웹 서버, 컨테이너, 파일 공유 서비스를 이것저것 올려보면서 Linux 보안을 강화하려다가 SELinux(Security-Enhanced Linux)와 AppArmor(Application Armor)를 번갈아 만져봤습니다. 처음엔 둘 다 비슷해 보였는데, 직접 써보니까 운영 철학부터 관리 포인트까지 꽤 다르더라고요.

    혹시 이런 경험 있으신가요? 서비스는 분명 정상인데 권한 거부 때문에 접속이 안 되고, 로그를 보면 더 헷갈리는 상황 말입니다. 저도 처음엔 이게 뭔가 싶었는데, 기준만 잡고 보면 선택이 꽤 쉬워집니다. 이번 글에서는 SELinux vs AppArmor를 비교하면서, 어떤 환경에서 무엇을 선택하면 좋은지 경험 기준으로 풀어보겠습니다.

    SELinux AppArmor 비교를 보여주는 리눅스 보안 개요 다이어그램

    SELinux와 AppArmor가 커널 보안 계층에서 어떻게 동작하는지 보여주는 개요 이미지입니다.

    1. 왜 SELinux와 AppArmor가 중요한가

    쉽게 말해 둘 다 MAC(Mandatory Access Control, 강제 접근 통제) 계열입니다. 일반적인 파일 권한처럼 사용자가 알아서 결정하는 DAC(Discretionary Access Control, 임의 접근 통제)보다 훨씬 강하게 제어하는 방식이거든요. 프로세스가 root 권한을 가졌다고 해도, 보안 정책이 막고 있으면 파일을 읽거나 네트워크 동작을 하지 못하게 되는 겁니다.

    이게 왜 중요하냐면, 서비스 하나가 뚫렸을 때 전체 서버로 번지는 걸 줄여주기 때문이에요. 예를 들어 웹 서버 프로세스가 취약점으로 악용되더라도, 원래 접근하면 안 되는 디렉터리나 소켓을 막아두면 피해 확산을 줄일 수 있습니다. 제가 홈랩에서 리버스 프록시, 파일 서버, 테스트용 앱을 굴릴 때도 결국 마지막 안전장치 역할은 이런 보안 정책이 하더라고요. 평소엔 귀찮아 보여도, 문제 생기면 존재감이 정말 확실합니다.

    2. SELinux와 AppArmor 개념 설명

    2-1. SELinux는 라벨 기반입니다

    SELinux는 객체와 프로세스에 보안 컨텍스트(context, 보안 문맥) 라벨을 붙여서 제어합니다. 파일, 포트, 프로세스가 각각 어떤 타입인지 보고 허용 여부를 판단하는 방식이죠. 처음엔 정말 어렵습니다. 저도 파일 권한만 보다가 갑자기 컨텍스트를 보라니까 삽질 좀 했어요. 그런데 익숙해지면 정책이 꽤 정교하고 체계적이더라고요.

    • 파일과 디렉터리에 보안 라벨을 부여합니다.
    • 프로세스도 특정 도메인(domain, 실행 문맥)으로 실행됩니다.
    • 정책이 세밀해서 대규모 운영 환경에 잘 맞는 편입니다.

    2-2. AppArmor는 경로 기반입니다

    AppArmor는 파일 경로(path, 경로)를 기준으로 제어합니다. 그래서 사람이 읽고 이해하기가 상대적으로 훨씬 쉽습니다. 특정 실행 파일이 어느 경로를 읽고 쓸 수 있는지 프로파일(profile, 정책 파일)로 정의하는 방식이거든요. Ubuntu 계열에서 처음 보안 강화 기능을 접할 때 부담이 덜한 이유가 바로 여기에 있습니다.

    • 실행 파일별 프로파일을 적용합니다.
    • 경로 기반이라 정책을 읽기 쉽습니다.
    • 처음 도입할 때 학습 곡선이 비교적 완만합니다.

    3. SELinux와 AppArmor 비교: 무엇이 다른가

    여기서 중요한 포인트! SELinux AppArmor 비교를 할 때 단순히 “누가 더 강한가”로 보면 답이 잘 안 나옵니다. 실제로는 운영 방식에 맞는지가 훨씬 더 중요하거든요.

    항목 SELinux AppArmor
    정책 기준 라벨 기반 경로 기반
    학습 난이도 상대적으로 높음 상대적으로 낮음
    정책 세밀도 매우 세밀함 직관적이고 단순한 편
    대표 사용 환경 RHEL 계열에서 자주 사용 Ubuntu 계열에서 자주 사용
    초기 운영 체감 로그 분석이 중요 프로파일 관리가 중요
    적합한 상황 엄격한 통제, 장기 운영 빠른 도입, 단순한 서비스 보호

    제 경험상 정리하면 이렇습니다.

    • SELinux: 처음엔 어렵지만, 표준화된 운영과 세밀한 정책이 필요한 환경에서 정말 강합니다.
    • AppArmor: 빠르게 적용하고 이해하기 쉬워서 소규모 서비스나 초기 구축에 부담이 훨씬 적습니다.
    • 보안 강도만 볼 게 아니라, 팀이 정책을 유지할 수 있는지가 더 중요한 선택 기준이 됩니다.

    4. 실전 구현: SELinux 기본 확인과 적용

    이제 실전으로 가보겠습니다. 제가 직접 해보니 무조건 정책부터 건드리기보다, 현재 상태를 먼저 보는 게 훨씬 낫습니다. SELinux는 특히 더 그렇습니다. 상태 확인 없이 파일만 만지면 나중에 왜 막혔는지 추적이 훨씬 어려워지거든요.

    1. SELinux 상태를 확인합니다.
    2. 서비스 파일 컨텍스트를 점검합니다.
    3. 필요하면 적절한 컨텍스트를 다시 지정합니다.
    4. 로그를 보고 실제 차단 원인을 확인합니다.
    getenforce
    seststatus
    ls -Z /var/www
    ps -eZ | grep httpd

    getenforce는 현재 모드를 빠르게 보여줍니다. Enforcing이면 정책이 실제로 강제 적용 중이고, Permissive면 차단 대신 로그만 남깁니다. 처음 테스트할 땐 Permissive 모드에서 원인부터 보는 게 훨씬 편하더라고요.

    sudo semanage fcontext -a -t httpd_sys_content_t "/srv/myweb(/.*)?"  
    sudo restorecon -Rv /srv/myweb

    이 명령은 웹 서버 콘텐츠 디렉터리에 적절한 타입을 지정하는 예시입니다. 여기서 정말 중요한 건 chmod로 해결하려고 하지 말고 컨텍스트를 봐야 한다는 점이에요. 저도 예전에 권한은 맞는데 계속 403이 떠서 한참 헤맸는데, 알고 보니 파일 라벨이 안 맞았던 경우가 정말 많았거든요.

    sudo ausearch -m avc -ts recent
    sudo journalctl -t setroubleshoot --since today

    SELinux는 로그를 보는 습관이 정말 핵심입니다. 거부 로그를 보면 무엇이 막혔는지 힌트를 얻을 수 있어요. 드디어 됐다! 하는 순간이 대부분 여기서 옵니다.

    SELinux AppArmor 비교 글의 SELinux 컨텍스트 적용 흐름 이미지

    SELinux에서 컨텍스트를 확인하고 복구하는 흐름을 단계별로 보여주는 이미지입니다.

    5. 실전 구현: AppArmor 기본 확인과 프로파일 적용

    AppArmor는 상대적으로 진입 장벽이 훨씬 낮습니다. 실제로 써보니까 정책 파일이 사람이 읽기 쉬워서, 빠르게 감을 잡기 정말 좋더라고요. 특히 단일 서비스 보호 용도로는 꽤 편했습니다.

    1. AppArmor가 활성화되어 있는지 확인합니다.
    2. 현재 로드된 프로파일과 적용 상태를 봅니다.
    3. 필요한 프로파일을 enforce 모드로 전환합니다.
    4. 로그를 보고 누락된 권한을 보완합니다.
    sudo aa-status
    sudo apparmor_status

    배포판에 따라 둘 중 하나를 쓰게 되는 경우가 있습니다. 출력에서 어떤 프로파일이 enforce 모드인지, complain 모드인지 확인하면 됩니다. complain은 차단 대신 기록만 남기는 모드라서 테스트할 때 정말 유용해요.

    sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
    sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx

    AppArmor는 이렇게 프로파일 단위로 전환하는 방식이 직관적입니다. 경로 기반이라 “이 서비스가 이 디렉터리를 왜 못 읽지?”를 추적할 때 상대적으로 수월했어요. 다만 경로 변경이 잦은 환경에서는 프로파일 관리가 생각보다 귀찮을 수 있습니다.

    sudo journalctl -k | grep DENIED
    sudo dmesg | grep apparmor

    차단 로그도 꼭 봐야 합니다. AppArmor는 쉬워 보이지만, 결국 로그를 안 보면 감으로 수정하게 되거든요. 그럼 나중에 정책이 지저분해져서 관리가 힘들어집니다.

    6. 어떤 환경에서 무엇을 선택할까

    SELinux AppArmor 비교의 결론은 기술 우열보다 운영 적합성에 있습니다. 제가 홈랩과 실무성 테스트 환경에서 느낀 기준을 정리해보면 이렇습니다.

    • SELinux를 권하고 싶은 경우: RHEL 계열을 쓰고 있고, 정책 일관성과 세밀한 제어가 중요할 때
    • AppArmor를 권하고 싶은 경우: Ubuntu 계열을 쓰고 있고, 빠르게 프로파일을 읽고 조정해야 할 때
    • 팀 운영 관점: 로그 분석과 정책 유지에 익숙한 팀이면 SELinux도 충분히 잘 굴릴 수 있습니다.
    • 개인 홈랩 관점: 처음 시작할 땐 AppArmor가 덜 부담스러울 수 있어요.

    사실 제가 직접 써봤을 때 느낀 체감은 이랬습니다. SELinux는 한 번 체계가 잡히면 든든하고, AppArmor는 처음 붙을 때 편하다는 거죠. 둘 다 좋은 도구인데, 도구보다 운영자의 숙련도가 더 크게 작용하더라고요.

    SELinux AppArmor 비교 선택 가이드를 위한 의사결정 이미지

    배포판, 운영 인력, 정책 복잡도에 따라 어떤 선택이 맞는지 보여주는 결정 흐름도입니다.

    7. ⚠️ 주의사항과 트러블슈팅

    이 섹션은 정말 중요합니다. 저도 처음엔 보안 기능을 꺼버리고 끝내고 싶었던 순간이 정말 많았거든요. 근데 그 습관 들이면 나중에 훨씬 더 힘들어집니다.

    7-1. 서비스가 안 되면 바로 비활성화하지 마세요

    가장 흔한 실수가 “일단 꺼서 되게 만들자”라는 생각입니다. 테스트 중 잠깐 상태를 완화하는 건 가능하지만, 영구 비활성화는 정말 신중해야 합니다. 문제 원인을 안 보고 끄면 이후에 보안 사고 대응력이 확 떨어지거든요.

    7-2. 파일 권한과 보안 정책을 혼동하지 마세요

    SELinux에서는 파일 권한이 맞아도 컨텍스트가 틀리면 접근이 막힙니다. AppArmor에서는 프로세스가 파일 경로 접근 권한을 갖고 있는지 별도로 봐야 합니다. 즉, chmod와 chown만으로는 완벽하게 해결되는 게 아닙니다.

    7-3. 로그 없는 수정은 거의 실패합니다

    • SELinux: AVC 거부 로그 확인
    • AppArmor: DENIED 로그 확인
    • 정책 변경 전후로 무엇이 달라졌는지 기록

    제가 실무 비슷한 환경에서 자주 하는 방식은 간단합니다. 먼저 로그를 보고, 그다음 가장 좁은 범위로 수정합니다. 한 번에 크게 열어버리면 편하긴 한데, 나중에 왜 열었는지 기억이 안 나더라고요.

    8. 검증과 결과 확인

    설정을 끝냈다면 반드시 검증해야 합니다. 적용했다고 믿는 것과 실제로 동작하는 건 정말 다르거든요. 여기서 확인할 건 세 가지입니다.

    1. 서비스가 정상 동작하는지 확인합니다.
    2. 보안 정책이 실제로 enforce 중인지 확인합니다.
    3. 거부 로그가 불필요하게 반복되지 않는지 봅니다.
    curl -I http://localhost
    getenforce
    sudo aa-status
    sudo journalctl -k --since "10 minutes ago"

    정상 응답이 오고, 보안 모드가 의도한 상태이며, 로그에 반복적인 거부가 없다면 1차 검증은 통과입니다. ✅ 여기까지 오면 운영 가능한 상태에 꽤 가까워진 겁니다. 저도 이 단계까지 정리해두면 이후 서비스 추가가 훨씬 수월했어요.

    SELinux AppArmor 비교 적용 후 검증 결과를 보여주는 대시보드 이미지

    서비스 정상 응답, 정책 적용 상태, 로그 감소 여부를 한눈에 확인하는 결과 이미지입니다.

    9. 정리와 FAQ

    SELinux vs AppArmor를 한 줄로 요약하면 이렇습니다. 세밀한 통제와 표준 운영이면 SELinux, 빠른 이해와 간결한 관리면 AppArmor를 선택하는 게 맞습니다. 물론 현실에선 배포판 기본값과 팀 경험이 선택에 큰 영향을 줍니다. 그래서 제 추천은 무조건 하나를 고집하기보다, 현재 운영 체계와 문제 해결 방식에 맞추는 거예요.

    다음 단계로는 컨테이너 환경에서의 보안 프로파일, systemd 서비스 하드닝, seccomp(시스템 콜 필터링) 같은 주제까지 이어가면 좋습니다. 이 부분은 다음 글에서 다룰 예정입니다. 이전 글에서 다뤘던 리눅스 권한 구조와 함께 보시면 흐름이 더 잘 잡히실 거예요.

    자주 묻는 질문

    • Q. 둘 중 하나만 꼭 써야 하나요?
      A. 보통은 배포판과 운영 표준에 맞춰 하나를 중심으로 관리하는 편이 현실적입니다.
    • Q. 초보자에게는 뭐가 더 쉬운가요?
      A. 대체로 AppArmor가 더 직관적입니다. 다만 장기 운영 기준에선 SELinux를 배워둘 가치가 정말 큽니다.
    • Q. 보안 기능이 성능을 크게 떨어뜨리나요?
      A. 일반적인 서버 운영에서 체감보다 정책 정확성이 더 큰 이슈인 경우가 많았습니다.
    SELinux AppArmor 비교 장단점을 정리한 인포그래픽

    선택 기준, 장단점, 추천 시나리오를 한 장으로 정리한 요약 인포그래픽입니다.

    마지막으로, SELinux AppArmor 비교를 하다 보면 자꾸 정답 하나를 찾게 되는데요. 실제로 써보니까 정답은 환경마다 달랐습니다. 중요한 건 꺼두는 게 아니라 이해하고 운영하는 거예요. 처음엔 헷갈려도, 로그를 읽고 정책을 조금씩 줄여가다 보면 감이 옵니다. 그 과정이 조금 번거롭긴 해도, 서버를 오래 안정적으로 굴리려면 결국 한 번은 넘어야 할 산이더라고요. 이거 정말 중요합니다.

  • [Proxmox] Proxmox LXC 컨테이너 보안 강화 체크리스트: 프로덕션 필수 설정

    [Proxmox] Proxmox LXC 컨테이너 보안 강화 체크리스트: 프로덕션 필수 설정

    Proxmox LXC 컨테이너 보안 강화 체크리스트: 프로덕션 필수 설정

    안녕하세요, 13년차의 서버실 주인장입니다. 오늘은 제가 홈랩에서, 그리고 실제 프로덕션 환경에서 Proxmox LXC 컨테이너를 운영하면서 뼈저리게 느꼈던 보안 강화에 대한 이야기를 해보려고 합니다. Proxmox LXC는 가볍고 빠르며 효율적이라서 저도 정말 애용하는 기술 스택인데요. 근데 이 편리함 뒤에는 간과하기 쉬운 보안 위협이 숨어있다는 사실, 혹시 알고 계셨나요? ⚠️

    처음에는 LXC가 워낙 가볍고 VM(Virtual Machine)만큼 복잡하지 않으니까, ‘이 정도면 괜찮겠지?’ 하고 안일하게 생각했던 적도 있습니다. 하지만 몇 번의 삽질과 실제 보안 사고(아찔한 순간이었죠… 휴)를 겪으면서, 컨테이너 환경도 강력한 보안 대책이 필수적이라는 것을 깨달았죠. 특히 프로덕션 환경에서는 더더욱 그렇습니다.

    오늘은 제가 직접 해보고 효과를 본 Proxmox LXC 컨테이너 보안 강화 체크리스트와 베스트 프랙티스를 여러분께 멘토처럼 알려드릴게요. 저처럼 삽질하지 마시라고, 제가 겪었던 시행착오와 해결 과정까지 솔직하게 공유해드리겠습니다! 자, 그럼 시작해볼까요? 🎉

    Proxmox LXC 컨테이너 보안 강화를 위한 주요 구성 요소를 보여주는 개요 다이어그램

    Proxmox LXC 컨테이너 환경에서 보안 강화를 위한 주요 구성 요소와 상호작용을 보여주는 다이어그램입니다.

    1. LXC 컨테이너, 왜 보안이 중요할까요? (개념 설명)

    Proxmox VE (Virtual Environment)에서 LXC (Linux Containers)는 도커(Docker) 컨테이너와 VM의 중간 지점에 있다고 생각하시면 편합니다. VM처럼 완벽한 격리는 아니지만, 도커보다는 더 OS에 가까운 환경을 제공하죠. 호스트 OS의 커널을 공유하기 때문에 가상화 오버헤드가 적고 성능이 좋습니다. 하지만 바로 이 커널 공유 때문에 보안에 취약점이 생길 수 있습니다.

    만약 하나의 LXC 컨테이너가 해킹당하면, 공격자가 호스트 OS의 커널에 접근할 수 있는 경로를 확보하게 될 수도 있습니다. 이는 곧 다른 컨테이너나 심지어 호스트 시스템 전체까지 위험에 빠뜨릴 수 있다는 뜻이거든요. 그래서 LXC 컨테이너는 VM만큼은 아니더라도, 최소한의 보안 장치들을 꼭 마련해두는 것이 좋습니다. 제가 처음 이걸 알았을 때, 등골이 오싹했더랬죠.

    2. 실전 구현: Proxmox LXC 보안 강화 체크리스트

    자, 이제 실질적으로 어떤 작업을 해야 하는지 단계별로 살펴보겠습니다. 제가 직접 구축하면서 가장 효과적이라고 느꼈던 방법들 위주로 구성했어요.

    2.1. ✅ Unprivileged 컨테이너 사용 (권한 분리)

    가장 기본 중의 기본입니다. Unprivileged container (비특권 컨테이너)는 컨테이너 내부의 root 사용자가 호스트 시스템의 root 권한을 가지지 못하도록 격리하는 방식입니다. 이게 핵심이에요. 만약 컨테이너가 Compromise (침해)되더라도, 공격자가 호스트 시스템에 직접적인 root 권한을 행사하기 어렵게 만드는 거죠.

    새 LXC 컨테이너를 생성할 때 Proxmox UI에서 ‘Unprivileged container’ 옵션을 체크하거나, CLI에서는 --unprivileged 1 옵션을 추가하면 됩니다. 저는 주로 CLI로 작업하기 때문에 다음과 같이 명령어를 사용합니다.

    pct create 101 local:vztmpl/debian-11-standard_11.0-1_amd64.tar.zst \
      --hostname my-secure-lxc \
      --password mysecretpassword \
      --memory 512 --swap 512 \
      --rootfs local-lvm:8 \
      --unprivileged 1 # 여기가 중요!

    이렇게 컨테이너를 생성하면 Proxmox가 자동으로 UID/GID 매핑 설정을 해줍니다. /etc/subuid와 /etc/subgid 파일에 호스트 시스템의 사용자 ID와 그룹 ID를 컨테이너 내부의 ID에 매핑하는 규칙이 추가되죠. 처음엔 이게 뭔가 싶었는데, 결국 호스트와 컨테이너 간의 권한 분리 장치더라고요.

    2.2. ✅ AppArmor 프로파일 적용 (강제적 접근 제어)

    AppArmor (앱아머)는 Linux 커널의 MAC (Mandatory Access Control, 강제적 접근 제어) 보안 시스템 중 하나입니다. 특정 프로그램이 접근할 수 있는 파일, 네트워크 리소스 등을 미리 정의된 프로파일에 따라 제한합니다. 쉽게 말해, ‘이 프로그램은 딱 이것만 할 수 있어!’라고 미리 정해주는 거죠.

    Proxmox는 기본적으로 LXC 컨테이너에 AppArmor 프로파일을 적용하지만, 더 세밀하게 제어하고 싶을 때가 있습니다. 예를 들어, 웹 서버 컨테이너가 불필요한 시스템 파일에 접근하는 것을 막는 것이죠.

    현재 AppArmor 상태는 다음 명령어로 확인할 수 있습니다.

    sudo aa-status

    특정 컨테이너나 서비스에 대한 커스텀 AppArmor 프로파일을 작성하여 /etc/apparmor.d/ 디렉토리에 넣고, sudo apparmor_parser -r /etc/apparmor.d/my-lxc-profile 명령어로 로드하면 됩니다. 제가 예전에 특정 서비스가 계속 죽어서 살펴보니, AppArmor 프로파일 때문에 필요한 파일에 접근을 못 하고 있더라고요. aa-complain 모드로 변경해서 로그를 보면서 디버깅했던 기억이 납니다.

    2.3. ✅ UFW를 이용한 네트워크 방화벽 설정

    컨테이너 내부에서도 방화벽을 설정하는 것은 정말 중요합니다. UFW (Uncomplicated Firewall)는 iptables의 복잡한 규칙들을 좀 더 쉽게 관리할 수 있게 해주는 도구입니다. LXC 컨테이너 내부에서 UFW를 설치하고 활성화하여, 필요한 포트만 외부에 노출하도록 설정할 수 있습니다.

    # 컨테이너 내부에서 실행
    sudo apt update
    sudo apt install ufw -y
    sudo ufw enable
    sudo ufw default deny incoming # 기본적으로 모든 외부 접근 차단
    sudo ufw allow ssh # SSH (22번 포트) 허용
    sudo ufw allow http # HTTP (80번 포트) 허용
    sudo ufw allow https # HTTPS (443번 포트) 허용
    sudo ufw status verbose

    이렇게 설정하면 컨테이너 내부에서 불필요한 포트가 열려 외부 공격에 노출되는 것을 방지할 수 있습니다. 혹시 LXC 안에서 서비스가 안 열려서 헤매신 적 있나요? 거의 십중팔구 UFW나 iptables 때문일 겁니다. 💡

    LXC 컨테이너 내부에서 UFW 명령어를 사용하여 네트워크 방화벽 규칙을 설정하고 확인하는 CLI 화면

    LXC 컨테이너 내부에서 UFW 명령어를 사용하여 네트워크 방화벽 규칙을 설정하고 확인하는 CLI 화면입니다.

    2.4. ✅ 정기적인 시스템 업데이트 및 패치

    너무 당연한 이야기 같지만, 잊지 않고 꾸준히 해야 할 가장 기본적인 보안 수칙입니다. OS와 설치된 모든 패키지를 최신 상태로 유지하여 알려진 취약점을 제거해야 합니다. 저는 unattended-upgrades를 설정해서 자동으로 업데이트되도록 해두는 편입니다. 물론 중요한 서비스는 수동으로 확인하고 적용하지만요.

    # 컨테이너 내부에서 실행
    sudo apt update && sudo apt upgrade -y
    
    # 자동 업데이트 설정 (옵션)
    sudo apt install unattended-upgrades -y
    sudo dpkg-reconfigure --priority=low unattended-upgrades

    2.5. ✅ SSH 보안 강화

    컨테이너에 접근하는 주요 방법 중 하나가 SSH입니다. SSH 보안을 강화하는 것은 컨테이너 전체의 보안을 높이는 데 큰 역할을 합니다.

    • 비밀번호 대신 키 기반 인증 사용: 가장 강력한 방법입니다.
    • 기본 SSH 포트 변경 (22번 외 다른 포트): 기본적인 스캐닝 공격을 회피할 수 있습니다.
    • Root 로그인 비활성화: PermitRootLogin no 설정.
    • 강력한 비밀번호 정책: (비밀번호 인증을 사용해야 할 경우)

    /etc/ssh/sshd_config 파일을 수정하여 위 항목들을 적용할 수 있습니다. 변경 후에는 꼭 sudo systemctl restart sshd 명령어로 SSH 서비스를 재시작해주세요.

    2.6. ✅ 로깅 및 모니터링

    아무리 보안을 강화해도 100% 완벽할 수는 없습니다. 중요한 것은 이상이 발생했을 때 얼마나 빨리 알아차리고 대응하느냐입니다. 컨테이너 내부의 로그를 중앙 집중식으로 관리하고, 주요 지표들을 모니터링하는 시스템을 구축하는 것이 좋습니다.

    • syslog-ng 또는 rsyslog 설정: 호스트 또는 별도의 로깅 서버로 로그를 전송.
    • Prometheus + Grafana: CPU, 메모리, 네트워크 트래픽 등 리소스 사용량과 서비스 상태 모니터링.
    • Fail2ban: SSH 무차별 대입 공격(Brute-force attack)과 같은 침입 시도를 자동으로 차단.

    물론 이 모든 걸 한 번에 다 하기는 어렵겠지만, 중요한 서비스부터 차근차근 적용해보는 것이 좋습니다. 제가 처음 홈랩을 구성할 때 로깅과 모니터링을 소홀히 했다가, 나중에 문제 터지고 나서야 뒤늦게 붙잡고 후회했던 적이 많거든요. 😅

    3. ⚠️ 주의사항 및 트러블슈팅: 제가 겪었던 삽질들

    위에서 말씀드린 내용을 적용하다 보면 분명히 문제가 발생할 수 있습니다. 저도 그랬거든요. 몇 가지 흔한 문제와 해결 방법을 공유해 드릴게요.

    • UID/GID 매핑 오류로 인한 컨테이너 시작 실패:

      • 증상: 컨테이너가 시작되지 않거나, 내부에서 파일 권한 문제로 서비스가 실행되지 않습니다. lxc-start 로그를 보면 Operation not permitted 같은 에러가 보입니다.
      • 해결: /etc/subuid와 /etc/subgid 파일에 올바른 매핑이 설정되어 있는지 확인합니다. pct create 시 --unprivileged 1 옵션을 제대로 사용했는지도 중요하고요. 이미 생성된 컨테이너라면 /etc/pve/lxc/VMID.conf 파일의 lxc.idmap 설정을 확인해야 합니다. 제가 이걸로 반나절을 날린 적이 있습니다.
    • AppArmor 프로파일로 인한 서비스 장애:

      • 증상: 특정 서비스가 AppArmor 정책 때문에 필요한 파일에 접근하지 못해 실행되지 않거나 비정상적으로 종료됩니다.
      • 해결: sudo aa-status로 AppArmor 상태를 확인하고, 문제가 되는 프로파일을 sudo aa-complain /etc/apparmor.d/my-lxc-profile 명령어로 complain 모드로 변경합니다. 이 모드에서는 정책 위반 시 로그만 남기고 차단하지 않으므로, 로그를 확인하여 필요한 접근 권한을 프로파일에 추가한 후 다시 sudo aa-enforce로 enforce 모드로 전환합니다.
    • UFW 포트 블로킹으로 인한 서비스 접근 불가:

      • 증상: 분명히 서비스는 실행 중인데 외부에서 접근이 안 됩니다.
      • 해결: 컨테이너 내부에서 sudo ufw status verbose 명령어로 현재 UFW 규칙을 확인합니다. 필요한 포트가 ALLOW 되어 있는지 확인하고, 만약 막혀있다면 sudo ufw allow [PORT] 명령어로 허용해줍니다.

    4. 검증 및 결과 확인

    보안 설정을 마쳤다면, 제대로 적용되었는지 확인하는 과정이 필수입니다.

    1. 컨테이너 권한 확인:

      # 호스트에서 실행
      lxc-info -n VMID -p

      lxc.idmap 설정이 올바르게 되어 있고, unprivileged 컨테이너로 생성되었는지 확인합니다.

    2. AppArmor 상태 확인:

      # 호스트에서 실행
      sudo aa-status

      적용된 프로파일이 enforce 모드로 잘 동작하는지 확인합니다.

    3. UFW 방화벽 규칙 확인:

      # 컨테이너 내부에서 실행
      sudo ufw status verbose

      필요한 포트만 열려 있고, 나머지는 잘 차단되어 있는지 확인합니다.

    4. 외부 포트 스캔:

      # 외부 시스템에서 실행 (예: 공격자 시점)
      nmap -sV -p- [LXC_컨테이너_IP]

      실제 외부에서 접근했을 때 불필요한 포트가 열려있지 않은지 nmap 같은 도구로 스캔해보는 것도 좋은 방법입니다. 저는 이 과정을 통해 ‘드디어 됐다!’ 하는 뿌듯함을 느꼈습니다. 😄

    LXC 컨테이너 보안 설정 완료 후 nmap 스캔 결과 및 AppArmor 상태를 보여주는 대시보드

    LXC 컨테이너 보안 설정이 완료된 후, nmap 스캔을 통해 열린 포트를 확인하고 AppArmor 상태를 점검하는 결과 화면입니다.

    5. 마무리하며: 지속적인 관심이 중요합니다

    오늘은 Proxmox LXC 컨테이너의 보안을 강화하기 위한 핵심 체크리스트를 저의 경험을 녹여가며 알려드렸습니다. Unprivileged 컨테이너, AppArmor, UFW, 정기 업데이트, SSH 보안 강화, 로깅 및 모니터링까지, 이 여섯 가지만 잘 지켜도 여러분의 LXC 컨테이너는 훨씬 더 안전해질 겁니다. 🛡️

    사실 보안은 한 번 설정하고 끝나는 것이 아니라, 지속적인 관심과 관리가 필요한 영역입니다. 새로운 취약점은 계속해서 발견되고, 공격 기술도 진화하거든요. 그러니 늘 최신 보안 동향에 귀 기울이고, 여러분의 시스템을 꾸준히 점검해주시길 바랍니다.

    다음 글에서는 LXC 컨테이너 환경에서 SELinux (Security-Enhanced Linux) 적용 방안이나, IDS/IPS (침입 탐지/방지 시스템)를 구축하는 방법에 대해 좀 더 깊이 있게 다뤄볼까 합니다. 혹시 궁금한 점이나 ‘이런 내용도 다뤄줬으면 좋겠다!’ 하는 아이디어가 있다면 언제든지 댓글로 남겨주세요! 여러분의 안전하고 튼튼한 서버실을 응원합니다. 💪

    Proxmox LXC 컨테이너 보안 강화를 위한 주요 체크리스트 항목들을 시각적으로 요약한 인포그래픽입니다.