13년차의 서버실

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

[태그:] 리눅스 보안

  • [Linux] SELinux 실제 서비스 적용 시 흔한 실수와 해결 사례

    [Linux] SELinux 실제 서비스 적용 시 흔한 실수와 해결 사례

    안녕하세요, 13년차의 서버실 주인장입니다. 오늘도 제 홈랩에서 밤샘 삽질 끝에 얻은 귀한 경험을 풀어보려 합니다. 혹시 여러분의 서비스에 SELinux (Security-Enhanced Linux)를 적용했다가 예상치 못한 오류에 부딪혀 당황했던 경험 없으신가요? “분명 권한은 다 줬는데 왜 안 되는 거지?” 하고 머리 싸매본 적 있으시다면, 오늘 제 이야기가 조금이나마 도움이 될 거라고 확신합니다.

    SELinux는 리눅스 시스템의 보안을 강화하는 강력한 메커니즘이에요. 제대로 이해하지 못하고 적용하면 서비스 장애의 주범이 될 수도 있거든요. 저도 처음엔 이 녀석 때문에 꽤나 고생했던 기억이 납니다. “아니, 그냥 보안 모듈인데 왜 이렇게 복잡해?” 싶었죠. 하지만 그 복잡함 속에는 시스템을 훨씬 더 안전하게 지켜줄 잠재력이 숨어있더라고요.

    오늘은 제가 직접 겪었던 SELinux 적용 시의 흔한 실수들과 그 해결 과정을 솔직하게 공유해볼까 합니다. 특히 서비스 운영 환경에서 맞닥뜨릴 수 있는 실제 사례들을 중심으로 말이죠. 함께 SELinux의 벽을 넘어봅시다! ✅

    SELinux의 강제적 접근 제어(MAC) 작동 방식 개요 다이어그램

    SELinux는 리눅스 커널의 보안 모듈이에요. 강제적 접근 제어(MAC, Mandatory Access Control)를 구현해서 시스템 보안을 한 단계 올려주는 거죠. 기존의 임의적 접근 제어(DAC, Discretionary Access Control) 방식은 사용자나 그룹에게 권한을 주는 방식이고, SELinux는 모든 프로세스와 파일에 보안 컨텍스트(Security Context)를 할당해서 이 컨텍스트 간의 상호작용을 정책에 따라 엄격하게 통제합니다. 쉽게 말해, “이 프로세스는 이 파일에만 접근할 수 있어!” 하고 딱지를 붙여놓는 거라고 생각하시면 됩니다.

    SELinux는 크게 세 가지 모드로 작동합니다.

    • Enforcing (강제 모드): 정책 위반 시 접근을 차단하고 로그를 남깁니다. 가장 강력한 보안을 제공하지만, 잘못된 정책은 서비스 장애로 이어질 수 있어요.
    • Permissive (허용 모드): 정책 위반 시 접근을 허용하지만 로그를 남깁니다. 주로 SELinux 적용 전 정책 테스트나 트러블슈팅 단계에서 활용돼요.
    • Disabled (비활성화 모드): SELinux가 완전히 비활성화됩니다. 보안적인 측면에서 권장되지 않습니다.

    현재 SELinux 상태는 <code>sestatus 명령으로 확인할 수 있어요.

    sestatus
    

    만약 Enforcing 모드라면, 이제부터 SELinux의 강제적 통제 아래에 놓이게 되는 겁니다. 저도 처음에 이걸 모르고 Enforcing으로 바로 올렸다가 서비스가 통째로 멈춰서 식은땀을 흘렸던 기억이 생생하네요. 😅

    실전 적용의 시작: 기본 설정과 첫 삽질

    제가 운영하는 홈랩에는 Nginx 웹 서버가 돌고 있습니다. 어느 날 문득 “보안을 좀 더 강화해야겠다!” 싶어서 SELinux를 Enforcing 모드로 전환했죠. 그리고 Nginx가 8080 포트에서 잘 동작하는지 확인했는데… 맙소사, 502 Bad Gateway 에러가 뜨는 겁니다!

    분명 Nginx 설정 파일(nginx.conf)에는 listen 8080; 이라고 되어 있고, 방화벽(firewalld)에도 8080 포트를 열어줬는데 말이죠. 처음엔 Nginx 설정 문제인가 싶어 이리저리 뜯어봤지만, 아무리 봐도 문제는 없었어요. 결국 “SELinux 때문에 문제가 생겼을 거야!” 라는 감이 왔습니다.

    SELinux 관련 문제를 진단할 때 가장 먼저 해야 할 일은 Audit Log (감사 로그)를 확인하는 것이에요. SELinux는 정책 위반이 발생하면 /var/log/audit/audit.log 파일에 상세한 정보를 기록하거든요. 이 로그를 잘 해석하는 것이 SELinux 트러블슈팅의 핵심입니다.

    SELinux 정책 위반 발생 시 Audit Log에서 에러를 추출하고 분석하는 과정 흐름도

    Audit Log는 내용이 방대해서 특정 메시지를 찾기가 쉽지 않아요. 이때 유용한 도구가 바로 sealert와 audit2allow입니다. 먼저 audit.log에서 “denied” 키워드로 필터링해서 SELinux 관련 에러를 찾아봅시다.

    grep "denied" /var/log/audit/audit.log | tail -n 10
    

    이것만으로는 해석하기 어려울 때가 많거든요. 이때는 sealert 유틸리티가 큰 도움을 줍니다. sealert -a /var/log/audit/audit.log 명령을 실행하면, Audit Log에서 SELinux 관련 경고를 추출하여 사람이 읽기 쉬운 형태로 요약해주고, 심지어 해결책까지 제안해줄 때도 있어요.

    sudo yum install setroubleshoot-server # CentOS/RHEL 계열
    sudo apt install setroubleshoot-server # Debian/Ubuntu 계열 (패키지명 상이할 수 있음)
    
    sealert -a /var/log/audit/audit.log
    

    저의 Nginx 8080 포트 문제의 경우, Audit Log를 확인해보니 다음과 비슷한 메시지를 찾을 수 있었습니다.

    type=AVC msg=audit(1678886400.123:456): avc:  denied  { name_bind } for  pid=1234 comm="nginx" src=8080 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:port_t:s0 tclass=tcp_socket permissive=0
    

    여기서 중요한 부분은 denied { name_bind }, src=8080, scontext=system_u:system_r:httpd_t:s0 입니다. httpd_t 타입의 Nginx 프로세스가 8080 포트에 name_bind (바인딩) 하는 것을 SELinux가 거부했다는 뜻이거든요. 즉, Nginx 프로세스가 일반적으로 허용된 포트(80, 443 등)가 아닌 8080 포트를 사용하려고 해서 생긴 문제입니다.

    흔한 실수와 해결 사례

    자, 이제 실제 서비스 운영 환경에서 자주 겪을 수 있는 SELinux 문제들을 몇 가지 사례를 통해 알아보겠습니다.

    케이스 1: 특정 포트 접근 문제 (Nginx 8080 포트 바인딩 실패)

    위에서 언급했던 Nginx의 8080 포트 바인딩 문제 해결입니다. SELinux는 웹 서버(httpd_t)가 특정 포트 타입(http_port_t)에만 바인딩하도록 정책을 정해뒀어요. 8080 포트는 기본적으로 웹 서버용 포트가 아니라고 인식되어 접근이 거부된 것이죠. 이 문제를 해결하려면 8080 포트를 웹 서버용 포트 타입으로 추가해줘야 합니다.

    # 현재 http_port_t 타입에 어떤 포트들이 등록되어 있는지 확인
    sudo semanage port -l | grep http_port_t
    
    # 8080 포트를 http_port_t 타입으로 추가 (TCP)
    sudo semanage port -a -t http_port_t -p tcp 8080
    
    # 변경사항 확인
    sudo semanage port -l | grep http_port_t
    
    # Nginx 재시작
    sudo systemctl restart nginx
    

    semanage port -a는 새로운 포트를 추가할 때 쓰고, -m은 수정, -d는 삭제할 때 쓰거든요. 이렇게 SELinux 정책을 수정하고 나니 Nginx가 8080 포트에서 정상적으로 동작하는 것을 확인할 수 있었습니다. 💡

    `semanage` 명령어를 사용하여 SELinux 포트 및 파일 컨텍스트 정책을 추가하는 CLI 화면 예시

    semanage 명령은 SELinux 정책을 관리하는 데 매우 중요한 도구예요. 포트뿐만 아니라 파일 시스템 컨텍스트, 불리언(Boolean) 값 등 다양한 정책을 다룰 수 있거든요.

    케이스 2: 웹 서버 특정 디렉터리 접근 문제

    홈랩에서는 웹 서버의 기본 문서 루트(Document Root)를 /var/www/html 대신 /srv/www 같은 다른 경로로 변경해서 사용하는 경우가 많아요. 그런데 SELinux가 Enforcing 모드일 때, Nginx나 Apache가 /srv/www에 있는 파일을 읽지 못하는 문제가 발생할 수 있습니다.

    이유는 간단해요. SELinux는 /var/www/html 경로에 있는 파일들에 httpd_sys_content_t라는 웹 서버 콘텐츠용 보안 컨텍스트를 자동으로 할당하지만, /srv/www 같은 비표준 경로의 파일들은 다른 컨텍스트(예: default_t)를 가질 수 있기 때문이에요. 웹 서버 프로세스(httpd_t)는 httpd_sys_content_t 타입의 파일에만 접근이 허용되므로, 다른 컨텍스트의 파일에는 접근이 거부됩니다.

    # /srv/www 경로에 있는 파일들의 현재 컨텍스트 확인
    ls -Zd /srv/www /srv/www/*
    
    # /srv/www 경로에 httpd_sys_content_t 컨텍스트를 영구적으로 적용하는 정책 추가
    sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"  
    
    # 파일 시스템에 변경된 컨텍스트 적용
    sudo restorecon -Rv /srv/www
    
    # 변경사항 확인
    ls -Zd /srv/www /srv/www/*
    
    # Nginx 재시작
    sudo systemctl restart nginx
    

    semanage fcontext는 파일 시스템 컨텍스트를 정의하는 규칙을 추가하고, restorecon은 이 규칙에 따라 실제 파일 시스템의 컨텍스트를 바꾸는 명령이에요. -R 옵션은 재귀적으로, -v 옵션은 변경되는 내용을 자세히 출력해주거든요. 이렇게 컨텍스트를 바꿔주니 웹 서버가 새로운 경로의 콘텐츠를 문제없이 제공하기 시작했습니다. 🎉

    케이스 3: 스크립트 실행 권한 문제 (PHP-FPM 소켓 접근 실패)

    PHP 애플리케이션을 Nginx와 함께 사용할 때, Nginx가 PHP-FPM 소켓(/run/php-fpm/www.sock)에 접근하지 못해서 502 에러가 나는 경우가 있어요. Nginx는 httpd_t 타입, PHP-FPM은 php_t 타입으로 실행되는데, Nginx가 PHP-FPM 소켓에 접근하는 것이 SELinux 정책에 의해 막힐 수 있거든요.

    이런 문제는 보통 SELinux 불리언(Boolean) 값을 조정하여 해결해요. SELinux 불리언은 특정 기능의 활성화/비활성화를 제어하는 스위치 역할을 하거든요. 웹 서버가 PHP-FPM과 같은 FastCGI 애플리케이션과 통신할 수 있도록 허용하는 불리언이 존재합니다.

    # 관련 불리언 목록 확인 (httpd_can으로 시작하는 것들)
    sudo getsebool -a | grep httpd_can
    
    # httpd_can_network_connect_php 불리언이 on으로 설정되어 있는지 확인
    sudo getsebool httpd_can_network_connect_php
    
    # 만약 off라면, on으로 변경 (영구적으로 적용하려면 -P 옵션 추가)
    sudo setsebool -P httpd_can_network_connect_php on
    
    # Nginx 및 PHP-FPM 재시작
    sudo systemctl restart nginx php-fpm
    

    httpd_can_network_connect_php 불리언을 on으로 설정하면, httpd_t 타입의 프로세스가 php_t 타입의 소켓에 네트워크 연결을 시도할 수 있게 돼요. 이 외에도 httpd_can_network_connect, httpd_can_sendmail 등 다양한 불리언이 있으니, 필요한 기능이 제대로 동작하지 않을 때는 관련 불리언을 찾아보는 것이 중요합니다.

    SELinux 정책 관리 팁

    SELinux는 강력하지만 복잡한 만큼, 올바른 접근 방식이 중요해요. 제가 삽질하며 배운 몇 가지 팁을 공유할게요.

    1. Permissive 모드 활용: 새로운 서비스를 배포하거나 SELinux를 처음 적용할 때는 Permissive 모드로 시작하는 게 좋습니다. 이 모드에서는 정책 위반이 차단되지 않고 로그만 남기거든요. 어떤 정책 위반이 발생하는지 미리 파악하고 필요한 정책을 구축하는 데 유용해요.
      sudo setenforce 0 # Permissive 모드로 전환
      sudo setenforce 1 # Enforcing 모드로 전환
              
    2. audit2allow의 양날의 검: audit2allow는 Audit Log를 분석하여 필요한 SELinux 정책 모듈을 자동으로 생성해주는 강력한 도구예요. 하지만 너무 남용하면 보안 구멍을 만들 수 있으니 주의해야 합니다. 최소한의 권한만을 허용하는 정책을 신중하게 생성하고 적용해야 하거든요.
      # Audit Log에서 정책 위반 메시지를 추출하여 모듈 생성 (예시)
      grep "httpd" /var/log/audit/audit.log | audit2allow -M mynginx
      
      # 생성된 모듈 확인 (mynginx.te, mynginx.pp)
      # 정책을 설치
      sudo semodule -i mynginx.pp
      

      audit2allow로 생성된 .te 파일 내용을 반드시 검토하여 불필요하게 넓은 권한을 부여하지 않는지 확인해야 해요. 저는 처음엔 그냥 생성된 대로 다 적용했다가 나중에 “이게 뭐지?” 하고 다시 삭제했던 적도 많습니다. ⚠️

    3. 영구 적용과 임시 적용: setenforce는 재부팅 시 초기화되는 임시 설정이고, /etc/selinux/config 파일은 영구적인 설정이에요. setsebool 명령도 -P 옵션을 사용해야 영구적으로 적용되거든요. semanage 명령으로 추가된 정책은 기본적으로 영구 적용됩니다. 이 차이를 이해하고 적절하게 활용하는 것이 중요해요.
    SELinux의 Enforcing, Permissive, Disabled 세 가지 작동 모드를 비교하는 요약표

    SELinux의 세 가지 모드를 비교하여 어떤 상황에 어떤 모드를 사용해야 할지 다시 한번 정리해봤어요.

    모드 정책 위반 처리 로그 기록 여부 주요 사용 목적 권장 상황
    Enforcing 접근 차단 ✅ 예 최종 서비스 운영 환경 보안 강화 모든 정책이 검증된 안정적인 서비스
    Permissive 접근 허용 ✅ 예 정책 개발 및 트러블슈팅 SELinux 도입 초기, 문제 진단 시
    Disabled 제한 없음 ❌ 아니오 SELinux 비활성화 극히 제한적이며 보안 취약점 발생

    마무리: SELinux, 두려워 말고 친해지세요!

    처음 SELinux를 접하면 그 복잡함과 엄격함 때문에 거부감이 들 수 있어요. 저도 그랬거든요. 하지만 SELinux는 리눅스 시스템의 보안을 한 차원 높여주는 강력한 도구임에 틀림없습니다. 제 경험상, SELinux는 한 번 제대로 설정해두면 시스템의 견고함이 비교할 수 없을 정도로 좋아져요.

    SELinux 트러블슈팅의 핵심은 Audit Log를 꼼꼼히 분석하고, 필요한 최소한의 정책만 추가하거나 수정하는 것이에요. 그리고 sealert, semanage, restorecon, setsebool 같은 도구들을 능숙하게 다루는 것이 중요하죠. 처음에는 시행착오를 많이 겪겠지만, 꾸준히 연습하고 경험을 쌓으면 SELinux가 더 이상 두려운 존재가 아니라 든든한 보안 파트너가 될 겁니다.

    혹시 여러분도 SELinux 때문에 겪었던 재미있는(혹은 슬픈) 삽질 경험이 있다면 댓글로 공유해주세요! 저도 배우는 자세로 함께 고민해보겠습니다. 다음번에는 홈랩에서 컨테이너 환경에 SELinux를 적용하는 방법에 대해 다뤄볼까 합니다. 기대해주세요! 🚀

  • [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] Linux 6.9 LUKS suspend 보안 이슈: 디스크 암호화 키 노출과 대응 전략

    [Linux] Linux 6.9 LUKS suspend 보안 이슈: 디스크 암호화 키 노출과 대응 전략

    [보안] Linux LUKS suspend 보안 이슈와 대응 전략

    최근 Linux LUKS suspend 보안 이슈가 다시 크게 회자되는 이유가 있습니다. 평소엔 노트북 뚜껑만 닫고 다니는 분들이 많잖아요. 저도 홈랩하고 실사용 장비를 굴리다 보면 suspend(서스펜드, 절전) 의존도가 꽤 높거든요. 그런데 2026년 7월 기준 공개된 정보로 보면, Linux 6.9 이후 특정 조건에서 LUKS의 키 제거 기대가 깨질 수 있는 회귀(regression, 기능 후퇴)가 확인됐습니다. 제목만 보면 바로 대형 재난처럼 느껴질 수 있는데, 실제 영향 범위는 조금 더 정확하게 봐야 합니다. 이번 글에서는 Linux LUKS suspend 보안 관점에서 무엇이 문제인지, 어떤 사용자가 진짜 영향권인지, 그리고 지금 당장 운영에서 어떻게 대응해야 하는지 차근차근 정리해보겠습니다.

    특히 보조 키워드로 많이 붙는 커널 6.9 취약점, 디스크 암호화 문제, 콜드 부트 공격도 함께 연결해서 보셔야 맥락이 잡힙니다. 저도 처음엔 “잠깐, resume 때 비밀번호 다시 받으면 안전한 거 아니었나?” 싶었는데요. 파고들어 보니 그게 함정이더라고요.

    Linux LUKS suspend 보안 이슈의 키 메모리 흐름 개요 이미지

    LUKS 장치, 커널 키링, suspend/resume 흐름, 공격 표면을 한눈에 보여주는 개요 이미지입니다.

    1. 왜 이 이슈가 중요한가: 잠자기와 보안은 같은 얘기가 아닙니다

    쉽게 말해, 풀디스크 암호화(Full Disk Encryption, 전체 디스크 암호화)를 쓰더라도 부팅 후 이미 복호화된 키가 RAM(메모리)에 남아 있으면 물리 접근 공격의 표적이 될 수 있습니다. 여기서 자주 언급되는 게 cold boot attack(콜드 부트 공격, 전원 차단 직후 남아 있는 메모리 데이터를 노리는 공격)이에요. 오래된 개념처럼 보이지만, 물리 접근 위협 모델에서는 아직도 무시하면 안 됩니다.

    WithSecure가 2018년에 다시 크게 환기한 내용도 비슷합니다. 절전 상태의 장비는 생각보다 안전하지 않을 수 있다는 점이죠. 그리고 Linux 쪽에서는 오래전부터 cryptsetup luksSuspend를 이용해 서스펜드 직전 키를 커널 메모리에서 지우고, 복귀 시 다시 인증받는 흐름을 활용해 왔습니다. 문제는 이 기대가 Linux 6.9 이후 일부 흐름에서 더 이상 그대로 성립하지 않는다는 점입니다.

    2. 개념 먼저 잡고 가죠: LUKS suspend가 원래 하려던 일

    여기서 중요한 포인트가 있습니다. 일반적인 suspend-to-RAM(메모리에 유지하는 절전)은 원래 RAM 전원이 살아 있습니다. 그래서 그냥 뚜껑 닫는다고 암호화 키가 저절로 사라지지 않거든요. 이걸 보완하려고 luksSuspend가 있는 겁니다.

    원래 기대 동작은 이렇습니다.

    1. LUKS 매핑 장치를 suspend 합니다.
    2. 디스크 I/O를 멈춥니다.
    3. 볼륨 키(volume key, 실제 데이터 복호화에 쓰는 키)를 커널 메모리에서 제거합니다.
    4. resume 시 다시 패스프레이즈나 토큰으로 키를 넣습니다.

    man page에도 luksSuspend는 활성 장치를 중단하고 커널 메모리에서 암호화 키를 지운다고 설명돼 있습니다. 저도 예전엔 이 문장만 보고 꽤 든든하게 느꼈었는데, 실제 구현은 keyring(키링, 커널 내부 키 저장 메커니즘) 동작에 의존하는 부분이 있더라고요.

    3. Linux 6.9 이후 무엇이 달라졌나: 진짜 쟁점은 키링 수명입니다

    이번 이슈의 핵심은 LUKS 자체 포맷이 깨졌다가 아닙니다. 키를 커널 쪽으로 넘기는 과정에서 쓰는 thread keyring(스레드 키링)의 수명 관리 가정이 깨진 것에 가깝습니다. cryptsetup 2.8.7 release notes에 따르면, 이전 버전은 볼륨 키를 thread keyring에 둘 수 있었고, 원래는 프로세스 종료 시 사라질 것으로 기대했거든요. 그런데 일부 상황, 예를 들어 loop device(루프 디바이스) 할당 같은 경우에는 thread keyring이 남아 있을 수 있다고 명시했습니다.

    이 문장을 보고 저도 “아, 이건 생각보다 문제의 결이 명확하네” 싶었습니다. 즉, Linux 6.9 이후 회귀로 인해 luksSuspend를 호출해도 사용자가 기대한 시점에 키가 완전히 사라지지 않을 수 있다는 얘기입니다. resume 때 비밀번호를 다시 묻는 화면이 떠도, 그 사실만으로 키가 제대로 지워졌다는 증거는 아닙니다.

    정리하면 이렇습니다.

    항목 정상 기대 문제 상황
    LUKS 일반 사용 부팅 후 키가 메모리에 존재 가능 원래도 suspend 중 메모리 노출 위험 존재
    luksSuspend 사용 서스펜드 직전 키 제거 기대 Linux 6.9 이후 특정 흐름에서 제거 보장이 흔들림
    resume 인증 프롬프트 추가 보안 절차처럼 보임 키 제거 성공 여부를 단독으로 증명하진 못함

    4. 누가 실제로 영향받나: 모든 리눅스 노트북 사용자는 아닙니다

    이 부분은 꼭 선을 그어야 합니다. 영향 대상은 주로 cryptsetup-suspend 패키지나 직접 만든 suspend hook으로 luksSuspend를 써 온 사용자입니다. Debian 계열에서 관련 패키지를 쓰거나, Arch/openSUSE/NixOS처럼 직접 훅을 구성한 분들이 대표적이죠.

    반대로 그냥 “LUKS로 루트 디스크 암호화는 했고, 평소에는 일반 suspend만 쓴다” 수준이면, 엄밀히 말해 이번 회귀 이전에도 콜드 부트 공격 관점의 메모리 잔존 위험은 남아 있었습니다. 그래서 이번 이슈를 볼 때는 보안 기능이 있었다가 기대대로 동작하지 않게 된 회귀로 이해하는 게 맞습니다.

    Linux LUKS suspend 보안 영향 범위와 위협 모델 비교 이미지

    일반 LUKS 사용자와 luksSuspend 사용자, suspend-to-RAM과 hibernation의 차이를 비교하는 이미지입니다.

    5. 실전 점검: 내 시스템이 위험 구간인지 확인하는 방법

    실제로 써보니까 제일 먼저 해야 할 건 감으로 판단하지 않는 겁니다. 아래 순서대로 확인해 보세요.

    5-1. 커널과 cryptsetup 버전 확인

    uname -r
    cryptsetup --version

    여기서 커널이 6.9 계열 이상인지, 그리고 cryptsetup이 어떤 버전인지 먼저 봅니다. 2026년 7월 기준으로 공개된 cryptsetup 2.8.7 release notes에는 이 keyring handling changes가 명시돼 있거든요.

    5-2. luksSuspend 사용 여부 확인

    grep -R "luksSuspend\|cryptsetup-suspend" /etc/systemd /etc/pm /usr/lib/systemd 2>/dev/null
    systemctl list-unit-files | grep -i cryptsetup
    dpkg -l 2>/dev/null | grep cryptsetup-suspend || true
    rpm -qa 2>/dev/null | grep cryptsetup || true

    배포판마다 다르니 한 가지 명령만 믿으면 안 됩니다. 저도 예전에 systemd sleep hook 한 군데만 보고 안심했다가 다른 경로에서 동작하는 유닛을 놓친 적이 있었거든요. 삽질 좀 했습니다 ㅎㅎ

    5-3. 운영 정책 확인

    loginctl show-session $(loginctl | awk '/tty|seat|pts/ {print $1; exit}') -p IdleHint
    systemctl status sleep.target suspend.target hibernate.target

    장비가 실제로 suspend-to-RAM 위주인지, hibernation(하이버네이션, 디스크로 메모리 상태 저장 후 전원 차단)도 쓰는지 확인해 두세요. 여기서 대응 전략이 갈립니다.

    6. 대응 전략: 지금 당장 운영에서 추천하는 순서

    제가 직접 운영 기준으로 정리하면 우선순위는 이렇습니다.

    1. 위협 모델이 강하면 suspend-to-RAM을 끄고 hibernation 또는 shutdown으로 전환
    2. cryptsetup 2.8.7 이상 제공 여부를 배포판에서 확인
    3. resume 프롬프트만 보고 안전하다고 판단하지 않기
    4. 민감 장비는 pre-boot authentication(부팅 전 인증)과 물리 보안 정책을 같이 적용

    커널 문서에서도 hibernation 쪽은 RAM 전원이 계속 유지되는 suspend와 보안 성격이 다릅니다. 결국 메모리에 키가 안 남는 상태를 만들고 싶다면 RAM 전원을 살려두는 절전보다 하이버네이션이 훨씬 낫다는 얘기죠.

    제가 실무에서라면 이렇게 가겠습니다.

    시나리오 권장 대응 이유
    출장용 노트북 Hibernate 우선 물리 탈취와 콜드 부트 공격 위험 완화
    사내 데스크톱 업데이트 후 정책 재검토 물리 접근 통제가 상대적으로 쉬움
    홈랩 테스트 머신 재현 후 버전 비교 영향 범위 검증과 자동화 테스트에 적합
    고민감 데이터 장비 Suspend 금지에 가깝게 운영 편의성보다 보안 우선

    7. ⚠️ 주의사항과 트러블슈팅: 여기서 많이 헷갈립니다

    첫째, resume 때 암호를 다시 묻는다고 끝이 아닙니다. 이게 제일 헷갈립니다. 사용자 입장에서는 “복귀 시 비밀번호 입력창 떴네, 그럼 잘 잠겼겠지”라고 생각하기 쉬운데요. 이번 Linux LUKS suspend 보안 이슈는 바로 그 안심 포인트를 찌릅니다.

    둘째, page cache(페이지 캐시) 문제와 이번 keyring 문제를 섞어 보면 안 됩니다. cryptsetup 이슈 트래커에는 2023년 말부터 luksSuspend 후에도 최근 읽은 데이터가 페이지 캐시에 남아 접근 가능하다는 별도 논의가 있었습니다. 이것도 디스크 암호화 문제로 꽤 중요하지만, 이번 글의 핵심인 Linux 6.9 이후 키링 기반 키 제거 회귀와는 결이 다릅니다. 둘 다 “잠자기 전 잠금” 기대를 흔든다는 공통점은 있지만 원인은 다르더라고요.

    셋째, loop device를 쓰는 테스트는 오히려 문제를 드러내기 좋습니다. release notes에서 loop device 상황을 직접 언급하거든요. 홈랩에서 재현 실험할 때는 이 흐름을 일부러 써 보는 게 이해에 도움이 됩니다.

    Linux LUKS suspend 보안 점검을 위한 커널 버전과 cryptsetup 확인 이미지

    커널 버전, cryptsetup 버전, suspend hook을 실제로 점검하는 터미널 중심 이미지입니다.

    8. 검증과 결과: 무엇을 확인하면 되나

    완성된 결과 확인은 “업데이트했다”에서 끝나면 안 됩니다. 운영에서는 아래 체크리스트까지 봐야 합니다. 이거 진짜 중요하더라고요.

    1. 커널 버전과 cryptsetup 버전을 문서화했는가
    2. luksSuspend 사용 경로가 실제로 존재하는가
    3. 민감 장비의 절전 정책이 suspend인지 hibernate인지 분리됐는가
    4. 보안 가이드에 “resume 비밀번호 프롬프트는 충분조건이 아님”이 반영됐는가

    제가 이런 류 이슈를 볼 때 늘 하는 방식은 간단합니다. 기능 설명 문구가 아니라 실제 위협 모델 기준으로 재평가하는 겁니다. 특히 출장 장비, 연구 장비, 고객 데이터가 실린 노트북은 더 그렇습니다. “암호화했으니 괜찮다”가 아니라 “절전 중에도 괜찮은가?”를 따로 봐야 하거든요.

    Linux LUKS suspend 보안 관점의 suspend와 hibernate 비교 인포그래픽

    절전 방식별 메모리 키 잔존 위험과 운영 편의성을 비교한 요약 이미지입니다.

    9. 마무리: 이번 이슈가 남긴 교훈

    이번 Linux LUKS suspend 보안 이슈를 보면서 다시 느낀 건, 보안은 결국 기능 존재 여부가 아니라 실제 동작 검증이라는 점입니다. 저도 처음엔 “luksSuspend까지 붙여 놨으면 꽤 단단하겠네”라고 생각했었는데, 이런 회귀가 나오면 운영 가정 자체를 다시 써야 하더라고요.

    정리하면 이렇습니다.

    • Linux 6.9 이후 luksSuspend 기반 보호 기대가 흔들린 공개 이슈가 있다.
    • 영향 범위는 모든 LUKS 사용자가 아니라 해당 suspend 잠금 흐름을 구성한 사용자 쪽에 더 직접적이다.
    • 콜드 부트 공격 같은 물리 접근 위협을 진지하게 보는 환경이라면 suspend-to-RAM보다 hibernation이 낫다.
    • cryptsetup 2.8.7의 keyring handling 변경 사항을 배포판에서 꼭 확인해야 한다.

    다음 글에서는 실제 배포판별로 cryptsetup-suspend, systemd sleep hook, hibernate 정책을 어떻게 점검하고 바꾸는지 더 실전적으로 다뤄볼 예정입니다. 이전 글에서 다룬 디스크 암호화 운영 체크리스트와도 이어서 보시면 훨씬 이해가 쉬우실 겁니다.

    참고 링크

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

  • [Linux] Linux 사용자 관리: sudo 권한부터 계정 관리까지 1년 실전 가이드

    [Linux] Linux 사용자 관리: sudo 권한부터 계정 관리까지 1년 실전 가이드

    [Linux] Linux 사용자 관리: sudo 권한부터 계정 관리까지 1년 실전 가이드

    홈랩을 오래 굴리다 보면 결국 제일 자주 만지는 영역이 Linux 사용자 관리더라고요. 처음에는 계정 하나 만들고 <code>sudo만 붙여주면 끝인 줄 알았는데, 실제로 1년 정도 운영해보니 그게 시작이었습니다. 누가 어떤 권한을 가져야 하는지, 비밀번호 정책은 어디까지 강하게 가져갈지, SSH(Secure Shell, 원격 접속) 접근은 어떻게 나눌지 같은 문제가 계속 나오거든요. 특히 여러 대의 서버를 동시에 관리하면 작은 실수가 바로 보안 이슈로 이어질 수 있어서, 이 부분은 초반에 습관을 잘 잡는 게 정말 중요합니다.

    저도 처음엔 루트(root, 최고 관리자 계정)로 바로 접속해서 작업하던 시기가 있었는데요. 편하긴 한데, 실수 한 번이면 복구가 꽤 피곤했습니다. 그래서 지난 1년 동안은 일반 사용자 계정 기반으로 운영하고, 필요한 작업만 sudo로 올리는 방식으로 정리해봤습니다. 실제로 써보니까 관리 포인트가 명확해지고, 문제 추적도 쉬워지더라고요. 이번 글에서는 제가 직접 해보며 정리한 사용자 관리 기준, sudo 권한 설정 방법, 그리고 계정 운영할 때 자주 터지는 문제까지 한 번에 묶어서 공유해보겠습니다.

    여러 대의 Linux 서버에서 사용자, 그룹, sudo 권한, SSH 접근 흐름이 어떻게 연결되는지 보여주는 개요 이미지입니다.

    1. 왜 Linux 사용자 관리가 생각보다 중요한가

    쉽게 말해 Linux 사용자 관리는 “누가, 어디까지, 어떤 방식으로 시스템을 만질 수 있느냐”를 정하는 일입니다. 서버가 한 대일 때는 체감이 덜할 수 있는데, 두 대 세 대 넘어가고 NAS(Network Attached Storage, 네트워크 저장소)나 컨테이너 호스트까지 붙기 시작하면 이야기가 달라집니다. 계정을 아무 생각 없이 늘리면, 나중에 누가 어떤 변경을 했는지 찾기 어려워집니다.

    • 책임 추적(Accountability, 작업 책임 추적)이 쉬워집니다.
    • 최소 권한 원칙(Principle of Least Privilege, 필요한 만큼만 권한 부여)을 적용할 수 있습니다.
    • 루트 계정 직접 사용을 줄여서 사고 범위를 제한할 수 있습니다.
    • 퇴사자 계정, 테스트 계정, 임시 계정을 정리하기 쉬워집니다.

    여기서 중요한 포인트! Linux는 기본적으로 강력한 멀티유저(multi-user, 다중 사용자) 시스템입니다. 그런데 운영자가 그 특성을 제대로 활용하지 않으면, 그냥 “다들 sudo 되는 단일 사용자 환경”처럼 굴러가 버리더라고요. 저도 한동안 그렇게 썼었는데, 나중에 계정 관리가 꼬이니까 정리가 꽤 힘들었습니다.

    2. Linux 사용자 관리의 핵심 개념 정리

    저도 처음엔 헷갈렸는데, 아래 네 가지만 명확히 잡으면 절반은 끝입니다.

    2-1. 사용자(User)와 그룹(Group)

    사용자(User, 개별 계정)는 실제 로그인 주체이고, 그룹(Group, 권한 묶음)은 여러 사용자에게 공통 권한을 부여할 때 씁니다. 보통 운영은 사용자에게 직접 권한을 덕지덕지 붙이는 것보다, 그룹 설계를 먼저 하고 사용자를 그룹에 넣는 쪽이 관리가 훨씬 편하더라고요.

    2-2. sudo 권한

    sudo는 superuser do의 약자로, 일반 계정이 관리자 권한으로 특정 명령을 실행할 수 있게 해줍니다. 즉, 항상 루트로 로그인하지 않아도 필요한 순간에만 권한을 올릴 수 있는 거죠. 실제로 써보니까 이 방식이 사고 범위를 줄이는 데 확실히 도움이 됐습니다.

    2-3. 인증(Authentication)과 인가(Authorization)

    인증은 “누구냐”를 확인하는 것이고, 인가는 “무엇을 할 수 있느냐”를 정하는 겁니다. SSH 키 로그인, 비밀번호, MFA(Multi-Factor Authentication, 다중 인증) 같은 건 인증 쪽이고, sudoers 설정은 인가 쪽에 가깝습니다.

    2-4. 관련 파일

    파일 역할 실무 포인트
    /etc/passwd 사용자 기본 정보 로그인 셸, 홈 디렉터리 확인
    /etc/shadow 비밀번호 해시 저장 권한 제한 필수
    /etc/group 그룹 정보 권한 설계 시 자주 확인
    /etc/sudoers sudo 정책 직접 수정 시 반드시 visudo 사용
    /etc/sudoers.d/ sudo 분리 설정 서버 역할별 관리에 유리

    사실 Linux 계정 관리에서 제일 중요한 건 명령어를 많이 아는 것보다, 정책을 일관되게 가져가는 것입니다. 같은 팀인데 서버마다 sudo 정책이 다르면, 그게 더 큰 장애 포인트가 되더라고요.

    3. 실전 구현: 계정 생성부터 그룹 설계까지

    제가 홈랩에서 가장 많이 쓰는 방식은 “개인 사용자 계정 + 역할 그룹 + 필요한 sudo만 부여” 구조입니다. 아래 예시는 Debian/Ubuntu 계열에서도 이해하기 쉽고, 다른 배포판에서도 거의 같은 개념으로 적용됩니다.

    1. 관리용 그룹을 먼저 만듭니다.
    2. 사용자 계정을 생성하고 홈 디렉터리를 함께 준비합니다.
    3. 필요한 그룹에 사용자를 추가합니다.
    4. SSH 키 인증을 붙입니다.
    5. 마지막으로 sudo 정책을 분리 파일로 관리합니다.

    3-1. 사용자 생성

    sudo adduser minsu
    sudo passwd minsu
    id minsu
    getent passwd minsu

    adduser는 대화형으로 홈 디렉터리와 기본 설정을 함께 잡아줘서 초반엔 편합니다. 반대로 자동화가 필요할 때는 useradd를 더 자주 쓰게 되더라고요.

    3-2. 그룹 기반으로 권한 묶기

    sudo groupadd ops
    sudo usermod -aG ops minsu
    id minsu
    getent group ops

    여기서 -aG 옵션을 빼먹으면 기존 보조 그룹이 날아갈 수 있습니다. 저 이거 한 번 놓쳐서 기존 접근 권한이 꼬인 적이 있었는데요. 드디어 됐다 싶었는데, 다른 작업 권한이 사라져 있더라고요. 그 뒤로는 id 사용자명으로 바로 검증하는 습관을 붙였습니다.

    Linux 사용자 관리와 sudo 권한 설정 흐름을 보여주는 이미지

    사용자 생성, 그룹 추가, sudoers.d 정책 분리까지 이어지는 실전 구성 흐름을 보여주는 이미지입니다.

    3-3. SSH 디렉터리와 권한 정리

    sudo mkdir -p /home/minsu/.ssh
    sudo chmod 700 /home/minsu/.ssh
    sudo touch /home/minsu/.ssh/authorized_keys
    sudo chmod 600 /home/minsu/.ssh/authorized_keys
    sudo chown -R minsu:minsu /home/minsu/.ssh

    SSH 키 로그인은 보안 측면에서 거의 기본값처럼 가져가는 게 좋습니다. 특히 외부에서 접속하는 서버라면 비밀번호 로그인만 믿고 가는 건 꽤 불안하거든요. 혹시 이런 경험 있으신가요? 키는 넣었는데 접속이 안 돼서 한참 헤매는 경우요. 대부분은 파일 권한이나 소유권 문제였습니다.

    4. sudo 권한 설정: 편하게 쓰되 넓게 주지는 않기

    sudo 권한은 편리하지만, 잘못 주면 사실상 루트와 다를 게 없어집니다. 그래서 저는 요즘 /etc/sudoers를 직접 건드리기보다 /etc/sudoers.d/ 아래에 역할별 파일을 나눠서 관리합니다. 이게 나중에 보기 훨씬 좋고, 변경 이력 추적도 편하더라고요.

    4-1. visudo로 안전하게 편집

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

    예시 파일은 이렇게 구성할 수 있습니다.

    %ops ALL=(ALL:ALL) ALL

    이 설정은 ops 그룹 사용자에게 모든 명령에 대한 sudo 실행 권한을 주는 거거든요. 홈랩이나 소규모 환경에선 현실적인 출발점이긴 한데, 실제 운영에서는 더 좁히는 게 좋습니다.

    4-2. 특정 명령만 허용하는 방식

    Cmnd_Alias SERVICE_MGMT = /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx
    %ops ALL=(root) SERVICE_MGMT

    처음엔 이런 세분화가 좀 귀찮았습니다. 근데 실제로 써보니까 “누구나 뭐든지 sudo 가능” 상태보다 훨씬 안정적이더라고요. 특히 여러 사람이 함께 관리하는 환경에서는 이 차이가 큽니다. 쉽게 말해, sudo는 켜고 끄는 스위치가 아니라 정교하게 나눠야 하는 운영 정책에 가깝습니다.

    4-3. 비밀번호 요구 정책도 점검

    Defaults logfile=/var/log/sudo.log
    Defaults lecture=once

    서버마다 필요는 다르겠지만, 누가 어떤 sudo 명령을 썼는지 남기고 싶다면 로그 정책도 함께 보는 게 좋습니다. 나중에 문제 생겼을 때 이 로그가 꽤 유용합니다. 이거 진짜 편하더라고요.

    5. 계정 관리 운영 팁: 1년 써보니 결국 정리 습관이 남습니다

    계정 관리는 만들 때보다 치울 때 실력이 드러납니다. 테스트 계정, 임시 외주 계정, 스크립트용 서비스 계정(service account, 서비스 전용 계정)이 뒤섞이기 시작하면 금방 지저분해집니다. 제가 1년 동안 정리하면서 체감한 운영 팁은 아래와 같습니다.

    • 개인 계정과 서비스 계정을 섞지 않습니다.
    • 공용 계정을 최소화합니다. 가능하면 개인 계정 기반으로 갑니다.
    • sudo 권한은 그룹 단위로 관리합니다.
    • 서버 접속 방식은 SSH 키 중심으로 통일합니다.
    • 퇴역 계정은 잠금(lock) 후 일정 기간 뒤 삭제하는 식으로 단계적으로 정리합니다.

    5-1. 계정 잠금과 만료 처리

    sudo passwd -l minsu
    sudo usermod -L minsu
    sudo chage -l minsu

    즉시 삭제보다 잠금부터 거는 이유는, 혹시 남아 있는 프로세스나 파일 소유권 이슈를 확인할 시간이 필요해서입니다. 처음엔 저도 바로 삭제했었는데, 나중에 cron(Cron, 예약 작업)이나 백업 스크립트에서 참조 중인 경우가 있더라고요.

    5-2. 정말 삭제할 때

    sudo userdel -r minsu

    단, -r 옵션은 홈 디렉터리까지 지우기 때문에 신중해야 합니다. 여기서 중요한 포인트! 삭제 전에 파일 소유권 검색은 꼭 한 번 해보세요.

    sudo find / -user minsu 2>/dev/null | head

    6. ⚠️ 실제로 많이 겪는 문제와 해결법

    이 섹션은 정말 경험담 위주입니다. 제가 직접 해보니, Linux 사용자 관리에서 막히는 포인트는 대체로 비슷했습니다.

    6-1. sudo가 안 되는 경우

    원인은 보통 세 가지였습니다.

    1. 사용자가 올바른 그룹에 들어가 있지 않음
    2. /etc/sudoers.d/ 파일 권한이나 문법 오류
    3. 새 그룹 적용 전에 세션 재로그인 안 함
    id minsu
    sudo visudo -c
    ls -l /etc/sudoers.d/
    newgrp ops

    visudo -c로 문법 검사를 먼저 해보면 생각보다 빨리 원인을 찾습니다. 저도 sudoers 문법 한 글자 잘못 넣고 한참 헤맨 적이 있었는데, 그때 깨달았습니다. sudo 설정은 “될 것 같은데 안 되는” 상태가 제일 사람 지치게 하더라고요.

    6-2. SSH 키는 맞는데 로그인 실패

    대부분은 권한 문제였습니다.

    • ~/.ssh 권한이 너무 넓음
    • authorized_keys 소유자가 다름
    • 홈 디렉터리 권한이 과하게 열려 있음

    이럴 때는 계정만 보지 말고 상위 디렉터리까지 같이 확인해야 합니다.

    6-3. 사용자 삭제 후 파일 찌꺼기 남음

    이건 생각보다 흔합니다. 파일 서버나 백업 경로에 예전 UID(User ID, 사용자 식별자) 소유 파일이 남아 있으면 나중에 권한 꼬임으로 이어질 수 있습니다. 삭제 전에 파일 검색, 삭제 후 UID 재사용 주의, 이 두 개는 꼭 기억해두면 좋습니다.

    Linux 사용자 관리 트러블슈팅과 sudo 권한 점검 장면 이미지

    sudo 문법 검사, 그룹 확인, SSH 권한 점검 같은 트러블슈팅 과정을 터미널 시점으로 보여주는 이미지입니다.

    7. 검증과 결과 확인: 설정은 했고, 이제 믿어도 되나?

    설정이 끝났다고 바로 안심하면 안 됩니다. 저는 마지막 검증 단계를 따로 두는 편입니다. 왜냐하면 계정과 권한은 “지금 된다”보다 “다음 로그인에서도 일관되게 된다”가 더 중요하거든요.

    7-1. 기본 검증 명령

    id minsu
    getent passwd minsu
    getent group ops
    sudo -l -U minsu
    su - minsu
    whoami
    sudo whoami

    여기서 기대 결과는 명확합니다. 일반 로그인 시에는 사용자 본인, sudo 실행 시에는 root가 나와야 합니다. 그리고 sudo -l -U 사용자명으로 허용된 명령 범위를 눈으로 확인하는 습관이 좋습니다.

    7-2. 운영 기준 체크리스트

    • 루트 직접 SSH 로그인 비활성화 여부 확인
    • 불필요한 계정 잠금 또는 삭제 완료
    • sudo 정책이 그룹 단위로 분리되어 있는지 확인
    • SSH 키 권한과 소유권 점검
    • 계정 생성/변경/삭제 절차를 문서화했는지 확인

    이렇게 정리하고 나니, 서버를 새로 추가해도 기준이 흔들리지 않았습니다. 예전에는 서버마다 계정 상태가 제각각이었는데, 지금은 최소한 “어디를 보면 되는지”가 정해져 있으니 정신이 훨씬 덜 없더라고요.

    Linux 사용자 관리 검증 결과와 체크리스트를 보여주는 이미지

    계정 상태, 그룹 소속, sudo 허용 범위, SSH 점검 항목을 한눈에 확인하는 검증 결과 이미지입니다.

    8. 정리와 다음 단계: Linux 사용자 관리, 결국 기본기가 다 합니다

    이번 글을 한 줄로 정리하면 이겁니다. Linux 사용자 관리는 명령어 몇 개 외우는 일이 아니라, 운영 기준을 만들고 지키는 습관입니다. 제가 1년 동안 계속 다듬어보니 가장 효과가 컸던 건 세 가지였습니다. 일반 사용자 계정으로 작업하기, sudo 권한을 그룹과 정책 파일로 분리하기, 그리고 계정 생애주기(lifecycle, 생성부터 삭제까지)를 끝까지 챙기기. 사실 화려한 기술은 아니지만, 이런 기본기가 서버 안정성을 꽤 오래 받쳐줍니다.

    다음 단계로는 PAM(Pluggable Authentication Modules, 인증 모듈) 정책, SSH 하드닝(hardening, 보안 강화), 그리고 중앙 인증까지 확장해보면 좋습니다. 이전 글에서 다뤘던 SSH 키 운영 방법이 있다면 같이 묶어보셔도 좋고, 다음 글에서는 sudo 로그와 감사(audit, 추적 기록) 중심으로 더 깊게 다뤄볼 예정입니다.

    자주 묻는 질문

    1. 루트 계정을 완전히 안 써도 되나요?
      완전히 안 쓴다기보다, 평소 작업은 일반 계정 + sudo로 돌리고 루트 직접 로그인 빈도를 최소화하는 쪽이 안전했습니다.
    2. sudo 권한은 사용자별로 주는 게 좋나요?
      짧게는 가능하지만, 운영이 길어질수록 그룹 기반 관리가 훨씬 덜 헷갈렸습니다.
    3. 계정 삭제 전에 꼭 확인할 건 뭔가요?
      파일 소유권, 예약 작업, SSH 키, 서비스 참조 여부입니다. 삭제보다 잠금부터 거는 방식이 실무에서 덜 위험했습니다.

    사용자, 그룹, sudo 권한, SSH 보안, 계정 정리 원칙을 한 장으로 요약한 마무리 인포그래픽입니다.

    결국 Linux 운영에서 사고를 줄이는 가장 현실적인 방법은, 계정과 권한부터 차분히 정리하는 겁니다. 화려하진 않지만 효과는 확실합니다. 저도 처음엔 이게 뭔가 싶었는데, 계속 손에 익히고 나니 서버 운영의 체력이 여기서 나온다는 걸 알겠더라고요.

  • [보안] Fail2ban 오탐 줄이기: 로그 분석과 정규식 최적화 전략

    [보안] Fail2ban 오탐 줄이기: 로그 분석과 정규식 최적화 전략

    [보안] Fail2ban 오탐 줄이기: 로그 분석과 정규식 최적화 전략

    Fail2ban 오탐 때문에 정상 사용자가 차단되는 상황, 한 번쯤 겪어보셨을 겁니다. 저도 홈랩이랑 외부에 노출된 몇몇 서비스에서 비슷한 Fail2ban 오탐 문제를 꽤 겪었습니다. 처음엔 “차단이 잘 되면 좋은 거 아닌가?” 싶었는데, 실제 운영에서는 이야기가 좀 다르더라고요. 특히 SSH, Nginx, 메일 서비스처럼 로그 형식이 조금만 달라져도 Fail2ban 정규식이 과하게 반응해서 멀쩡한 요청까지 공격으로 오인하는 경우가 생깁니다. 이번 글에서는 제가 실제로 정리해 둔 방식대로, 로그 분석부터 정규식(regex, 정규 표현식) 튜닝까지 단계별로 풀어보겠습니다.

    핵심은 단순합니다. 차단 수치를 무작정 올리거나 <code>maxretry만 만지는 게 아니라, 먼저 로그를 읽고 패턴을 이해한 다음, 그에 맞는 필터를 만드는 겁니다. 이 흐름만 잡히면 침입 방지 시스템을 좀 더 안정적으로 운영할 수 있습니다.

    Fail2ban 오탐 분석과 로그 처리 흐름을 보여주는 아키텍처 이미지

    Fail2ban 오탐을 줄이기 위해 로그 수집, 패턴 분석, 정규식 튜닝, 차단 검증까지 이어지는 전체 흐름을 한눈에 보여주는 이미지입니다.

    왜 Fail2ban 오탐이 생길까요?

    쉽게 말해 Fail2ban은 로그를 보고 판단합니다. 네트워크 패킷 자체를 해석하는 게 아니라, 서비스가 남긴 텍스트 로그를 기준으로 차단하거든요. 그래서 서비스 설정이 바뀌거나 프록시(Proxy, 중계 서버)가 앞단에 추가되면, 기존 필터가 예상하지 못한 문자열이 로그에 섞이게 됩니다.

    예를 들어 이런 경우가 대표적입니다.

    • 리버스 프록시(Reverse Proxy, 역방향 프록시) 뒤에 있어서 실제 클라이언트 IP 대신 프록시 IP가 보이는 경우
    • 애플리케이션 로그 포맷이 커스텀되어 기본 필터와 맞지 않는 경우
    • 에러 로그와 경고 로그가 비슷한 문장 구조를 가져서 실패 이벤트로 잘못 매칭되는 경우
    • 한글 메시지 또는 추가 모듈 로그가 섞여 정규식이 과하게 넓게 잡히는 경우

    저도 예전에 Nginx 뒤에 붙은 인증 서비스에서 Fail2ban 오탐이 계속 나서 삽질 좀 했습니다. 원인은 단순했어요. 기존 필터가 authentication failure 같은 문자열만 보고 잡고 있었는데, 실제로는 봇 공격이 아니라 브라우저 재시도 요청 일부까지 걸리고 있었거든요.

    Fail2ban 동작 원리와 정규식 최적화 포인트

    Fail2ban은 크게 세 가지를 봅니다. 필터(filter), 저널 또는 로그 소스(log source), 그리고 차단 정책(jail)입니다. 여기서 Fail2ban 오탐을 줄이는 핵심은 필터입니다.

    구성 요소 역할 오탐과의 관계
    Filter 로그에서 실패 패턴 추출 정규식이 넓으면 정상 로그도 공격으로 인식
    Jail 재시도 횟수, 차단 시간, 대상 서비스 설정 필터가 잘못되면 정책이 정상 사용자에게 적용
    Action iptables, nftables 등으로 차단 수행 실수한 필터가 실제 차단으로 이어짐

    여기서 중요한 포인트! 정규식은 많이 잡는 게 좋은 게 아닙니다. 정확하게 잡는 게 중요합니다.

    제가 보통 보는 기준은 이렇습니다.

    1. 실패 이벤트를 명확히 나타내는 고정 문자열이 있는가
    2. IP 주소 위치가 일정한가
    3. 정상 요청과 실패 요청을 구분하는 문장이 분리되는가
    4. 시간대별, 서비스별 로그 변형이 있는가

    이 네 가지만 점검해도 Fail2ban 오탐 확률이 꽤 줄어듭니다.

    로그 분석 먼저: Fail2ban 오탐 줄이기의 출발점

    정규식을 바로 고치기 전에 반드시 로그를 먼저 봐야 합니다. 이걸 건너뛰면 거의 감으로 튜닝하게 되는데, 그러면 나중에 더 크게 꼬입니다. 실제로 써보니까 로그 20줄 제대로 보는 게 설정 20번 바꾸는 것보다 훨씬 빠르더라고요.

    1. 최근 차단 이벤트 확인

    sudo fail2ban-client status
    sudo fail2ban-client status sshd

    이 명령으로 활성화된 jail과 현재 차단된 IP를 먼저 확인합니다. 어떤 jail이 유독 많이 반응하는지부터 보는 거죠.

    2. 원본 로그에서 실패 패턴 확인

    sudo tail -n 200 /var/log/auth.log
    sudo journalctl -u ssh --since "1 hour ago"
    sudo grep -i "fail\|invalid\|error" /var/log/auth.log | tail -n 50

    시스템마다 로그 경로는 다를 수 있습니다. Debian/Ubuntu 계열은 auth.log, systemd 환경은 journalctl 기반으로 보는 경우가 많습니다.

    3. 정상 로그와 실패 로그를 같이 비교

    이 단계가 진짜 중요합니다. Fail2ban 오탐은 보통 실패 로그만 보면 안 보입니다. 정상 사용자 접속, 키 교환, 세션 종료 같은 문장까지 같이 봐야 차이가 보이거든요.

    sudo grep -E "Failed password|Accepted password|Invalid user|Connection closed" /var/log/auth.log | tail -n 100

    제가 자주 하는 방식은 실패 케이스와 정상 케이스를 각각 10~20줄씩 복사해서 옆에 두고 비교하는 겁니다. 어떤 단어가 실패에서만 나오는지 찾는 거죠.

    Fail2ban 오탐을 줄이기 위한 정상 로그와 실패 로그 비교 이미지

    정상 로그인 로그와 실패 로그인 로그를 나란히 비교하면서 정규식에 포함해야 할 문자열과 제외해야 할 문자열을 구분하는 과정을 표현한 이미지입니다.

    실전 구현: Fail2ban 필터와 정규식 다듬기

    이제 본격적으로 Fail2ban 정규식을 손보겠습니다. 여기서는 SSH 계열 로그를 예시로 들지만, 원리는 Nginx나 메일 서비스에도 그대로 적용됩니다.

    1. 기존 필터 확인

    sudo ls /etc/fail2ban/filter.d/
    sudo sed -n '1,200p' /etc/fail2ban/filter.d/sshd.conf

    기존 필터를 무작정 덮어쓰는 건 추천하지 않습니다. 기본 파일은 참고만 하고, 별도 커스텀 필터를 만드는 편이 유지보수에 좋습니다.

    2. 커스텀 필터 작성

    # /etc/fail2ban/filter.d/sshd-local.conf
    [Definition]
    failregex = ^%(__prefix_line)sFailed password for (?:invalid user )?.* from <HOST> port \d+ ssh2$
                ^%(__prefix_line)sInvalid user .* from <HOST>$
    ignoreregex = ^%(__prefix_line)sConnection closed by authenticating user .*$
                  ^%(__prefix_line)sAccepted publickey for .*$

    여기서 의도는 명확합니다.

    • failregex는 실패로 확정할 수 있는 로그만 좁게 잡습니다
    • ignoreregex는 헷갈릴 수 있는 정상 또는 무해한 로그를 제외합니다
    • <HOST>는 Fail2ban이 IP를 추출할 때 사용하는 플레이스홀더입니다

    처음엔 이게 뭔가 싶었는데, 실제로는 ignoreregex를 잘 쓰는 순간 Fail2ban 오탐이 확 줄더라고요. 많은 분들이 failregex만 만지는데, 사실 둘을 같이 봐야 합니다.

    3. jail 설정 분리

    # /etc/fail2ban/jail.local
    [sshd-local]
    enabled = true
    filter = sshd-local
    port = ssh
    logpath = /var/log/auth.log
    maxretry = 5
    findtime = 10m
    bantime = 30m

    maxretry, findtime, bantime은 정책값입니다. Fail2ban 오탐이 자주 난다고 무조건 maxretry를 크게 올리면 실제 공격 대응이 느슨해질 수 있습니다. 그래서 저는 먼저 필터를 좁히고, 그 다음 정책을 조정하는 순서를 지킵니다.

    4. 정규식 테스트

    sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd-local.conf

    이 명령은 거의 필수입니다. 실제 로그 파일에 대해 내가 만든 정규식이 몇 줄을 매칭하는지 보여주거든요. 여기서 정상 로그가 잡히면 배포 전에 바로 알 수 있습니다. 드디어 됐다! 싶은 순간도 보통 여기서 옵니다.

    Fail2ban 정규식 테스트와 로그 분석 검증 화면 이미지

    fail2ban-regex 명령으로 필터가 어떤 로그를 매칭하는지 검증하고, 예상치 못한 정상 로그가 걸리는 부분을 찾아내는 과정을 시각화한 이미지입니다.

    ⚠️ 제가 실제로 겪었던 Fail2ban 오탐 패턴과 해결법

    운영하다 보면 생각보다 단순한 이유로 오탐이 납니다. 아래는 제가 자주 본 패턴입니다.

    프록시 뒤에서 IP가 꼬이는 경우

    Nginx 같은 리버스 프록시 뒤에 인증 서비스가 있으면, 애플리케이션 로그에 원본 IP 대신 프록시 IP가 찍히는 경우가 있습니다. 그러면 한 사용자의 실패가 아니라 프록시 전체가 문제처럼 보일 수 있죠. 이럴 땐 애플리케이션 로그 포맷에서 실제 클라이언트 IP가 어디에 기록되는지 먼저 확인해야 합니다.

    로그 문장이 비슷해서 정상 이벤트가 같이 잡히는 경우

    예를 들어 Connection closed 같은 문장은 실패 직후에도 보이고 정상 종료에도 보일 수 있습니다. 이런 문장을 넓게 잡아버리면 Fail2ban 오탐이 생깁니다. 그래서 저는 실패 판단용 문자열은 가급적 Failed, Invalid user처럼 의미가 분명한 것만 씁니다.

    한 줄 정규식으로 모든 걸 해결하려는 경우

    이거 정말 많이 봅니다. 정규식을 너무 똑똑하게 만들려고 하면 나중에 본인도 못 읽게 됩니다. 차라리 여러 줄의 failregex로 케이스를 나누는 게 유지보수에 훨씬 좋습니다.

    테스트 없이 서비스 재시작하는 경우

    저도 초반에 이 실수 했었습니다. 필터 바꾸고 바로 재시작했는데, 다음 날 정상 사용자 한 명이 접속이 안 된다고 연락이 오더라고요. 그 뒤로는 무조건 fail2ban-regex 테스트부터 합니다.

    검증 절차: 설정 후 무엇을 확인해야 할까

    설정을 넣었다고 끝이 아닙니다. Fail2ban 오탐은 운영 중에 드러나는 경우가 많아서, 적용 후 검증 루틴이 필요합니다.

    1. 필터 문법 테스트
    2. 실제 로그에 대한 매칭 수 확인
    3. 서비스 재시작 또는 설정 리로드
    4. 최근 1시간 로그에서 정상 이벤트가 차단 대상에 포함되는지 확인
    5. 차단된 IP 목록과 대응 로그를 대조
    sudo fail2ban-client reload
    sudo fail2ban-client status sshd-local
    sudo fail2ban-client get sshd-local banip

    가능하면 테스트 계정으로 정상 로그인과 실패 로그인을 각각 발생시켜 보는 것도 좋습니다. 홈랩 환경에서는 이런 검증을 해보기 좋아서, 저도 새 필터 만들면 꼭 직접 재현해 봅니다.

    검증 체크리스트

    • 정상 로그인 이벤트가 failregex에 잡히지 않는가
    • 실패 이벤트는 빠짐없이 잡히는가
    • 차단된 IP가 실제 공격 주체와 일치하는가
    • 로그 포맷 변경 시 필터가 깨질 가능성은 없는가

    결과 정리: Fail2ban 오탐 줄이기 전후 비교

    점검 항목 오탐 줄이기 전 오탐 줄인 후
    필터 범위 에러 비슷한 문자열을 넓게 포함 실패로 확정 가능한 문장만 포함
    정상 사용자 영향 재시도나 세션 종료 로그까지 차단 가능 정상 이벤트는 ignoreregex로 제외
    운영 안정성 차단 이유 추적이 어려움 왜 차단됐는지 로그와 정규식이 명확함
    유지보수 한 줄짜리 복잡한 정규식에 의존 케이스별로 읽기 쉬운 필터 분리

    결국 중요한 건 “많이 막는 것”보다 “정확히 막는 것”입니다. 서버 보안은 강하게만 간다고 좋은 게 아니더라고요. 특히 운영 환경에서는 정상 사용자의 경험도 같이 지켜야 하니까요.

    Fail2ban 오탐 감소 후 차단 결과를 검증하는 대시보드 이미지

    Fail2ban 오탐이 줄어든 뒤 차단 로그와 정상 접속 로그가 명확하게 구분되고, 관리자 입장에서 추적이 쉬워진 상태를 보여주는 결과 이미지입니다.

    자주 묻는 질문: Fail2ban 오탐 대응 FAQ

    Q1. maxretry만 올리면 해결되지 않나요?

    일시적으로는 덜 민감해질 수 있습니다. 근데 근본 해결은 아닙니다. 정규식이 잘못되면 정상 로그를 계속 실패로 판단하니까요.

    Q2. ignoreregex는 꼭 써야 하나요?

    반드시 필요한 건 아니지만, 실제 운영에서는 꽤 유용합니다. 특히 비슷한 형식의 정상 로그가 많은 서비스에서는 Fail2ban 오탐 방지에 효과가 큽니다.

    Q3. 로그 분석은 어느 정도까지 해야 하나요?

    최소한 정상 이벤트와 실패 이벤트를 각각 여러 줄씩 비교해 보시는 걸 권합니다. 한두 줄 보고 만들면 예외 케이스를 놓치기 쉽습니다.

    Q4. 기본 필터를 수정해도 되나요?

    가능은 하지만 권장하지 않습니다. 배포판 업데이트나 패키지 변경 시 추적이 어려워질 수 있어서, 별도 로컬 필터 파일로 분리하는 편이 낫습니다.

    마무리: 로그를 읽는 습관이 Fail2ban 운영을 살립니다

    이번 글에서는 Fail2ban 오탐을 줄이기 위해 로그를 어떻게 읽고, Fail2ban 정규식을 어떤 기준으로 다듬어야 하는지 정리해봤습니다. 저도 처음엔 단순히 차단 시간이랑 재시도 횟수만 조정했었는데, 결국 답은 로그 안에 있더라고요. 실제로 해보니까 정규식을 똑똑하게 짜는 것보다, 서비스 로그가 어떤 의미를 가지는지 먼저 이해하는 게 훨씬 중요했습니다.

    혹시 지금도 정상 사용자가 자꾸 차단돼서 골치 아프시다면, 오늘 바로 fail2ban-regex부터 돌려보세요. 거기서 의외로 실마리가 바로 나옵니다. 다음 글에서는 Nginx 액세스 로그 기준으로 봇 요청과 인증 실패를 분리해서 침입 방지 시스템 정책을 나누는 방법도 다뤄볼 예정입니다. 이전 글에서 방화벽 정책과 로그 로테이션(log rotation, 로그 순환) 정리해두셨다면 같이 보시면 흐름이 더 잘 잡히실 겁니다.

    로그 분석, 정규식 최소화, ignoreregex 활용, 테스트 검증이라는 네 가지 핵심 원칙을 한 장으로 정리한 요약 이미지입니다.