Certified Kubernetes Security Specialist (CKS) #7 seccomp 프로파일

#6 AppArmor 프로파일에서는 컨테이너가 어떤 파일에 접근하고 어떤 기능을 쓸 수 있는지를 프로파일로 묶었습니다. 같은 System Hardening도메인의 짝이 되는 도구가 seccomp입니다. AppArmor가 파일과 기능을 본다면, seccomp은 컨테이너가 커널에 던지는 시스템 콜 그 자체를 거릅니다. 이번 글에서는 seccomp의 개념과 세 가지 프로파일 타입, Pod에 적용하는 법, 노드에 커스텀 프로파일을 올려 참조하는 법, 그리고 차단을 검증하는 법까지 정리하겠습니다.

seccomp이란 #

**seccomp(secure computing mode)**은 리눅스 커널 기능으로, 프로세스가 호출할 수 있는 **시스템 콜(syscall)**을 제한합니다. 시스템 콜은 사용자 공간의 프로세스가 커널에 작업을 요청하는 유일한 통로입니다. 파일을 열고, 네트워크 소켓을 만들고, 새 프로세스를 띄우고, 커널 모듈을 적재하는 모든 동작이 시스템 콜로 이루어집니다. 리눅스에는 300개가 넘는 시스템 콜이 있고, 대부분의 컨테이너는 그중 극히 일부만 씁니다.

문제는 공격자가 컨테이너를 장악했을 때 나머지 시스템 콜을 자유롭게 쓸 수 있다는 점입니다. mount, keyctl, unshare, bpf 같은 시스템 콜은 권한 상승과 컨테이너 탈출에 직접 악용됩니다. seccomp은 컨테이너가 쓰지 않는 시스템 콜을 미리 막아 공격 표면을 시스템 콜 수준에서 좁힙니다.

seccomp 프로파일은 JSON 문서로, 기본 동작(defaultAction)을 정하고 예외가 되는 시스템 콜 목록을 나열하는 구조입니다. 가장 흔한 패턴은 “기본은 차단하되, 알려진 안전한 시스템 콜만 허용"입니다.

AppArmor와의 차이 #

seccomp과 AppArmor는 둘 다 System Hardening 도구이지만 막는 층위가 다릅니다.

항목seccompAppArmor
대상시스템 콜(syscall)파일 경로, capability, 네트워크
질문“이 시스템 콜을 허용할까”“이 파일을 읽거나 쓸까”
정의 위치JSON 프로파일텍스트 프로파일(/etc/apparmor.d/)
적용 키securityContext.seccompProfileannotation 또는 securityContext.appArmorProfile
적용 단위Pod 또는 containercontainer

둘은 경쟁 관계가 아니라 보완 관계입니다. seccomp으로 위험한 시스템 콜을 막고, AppArmor로 파일과 기능 접근을 묶으면 방어가 겹겹이 쌓입니다. 시험에서는 두 도구를 각각 다루지만, 실무에서는 함께 적용하는 것이 정석입니다.

세 가지 프로파일 타입 #

쿠버네티스의 seccompProfile.type은 세 가지 값을 가집니다.

type의미비고
RuntimeDefault컨테이너 런타임이 제공하는 기본 프로파일 적용containerd,CRI-O가 검증한 합리적 기본값. 권장
Localhost노드에 올려둔 커스텀 프로파일 파일 참조localhostProfile로 파일 경로 지정
Unconfinedseccomp 미적용. 모든 시스템 콜 허용사실상 무방비. 피해야 함

RuntimeDefault를 기본으로 #

가장 먼저 외울 것은 RuntimeDefault를 기본값으로 쓴다는 원칙입니다. containerd나 CRI-O 같은 런타임은 컨테이너 워크로드가 거의 쓰지 않는 위험한 시스템 콜을 막는 검증된 기본 프로파일을 내장하고 있습니다. 이 프로파일은 일반적인 애플리케이션을 깨뜨리지 않으면서 mount, reboot, keyctl 같은 위험한 시스템 콜을 차단합니다.

주의할 점은 쿠버네티스의 과거 기본값이 Unconfined였다는 사실입니다. seccomp을 명시하지 않은 Pod는 아무 시스템 콜 제한 없이 떴습니다. 이를 바로잡기 위해 kubelet에 --seccomp-default 플래그(또는 SeccompDefault 피처 게이트)를 켜면, 프로파일을 지정하지 않은 모든 Pod에 자동으로 RuntimeDefault가 적용됩니다. 시험에서 “노드의 모든 Pod에 기본 seccomp을 강제하라"는 작업이 나오면 이 플래그를 떠올려야 합니다.

securityContext.seccompProfile 설정 #

seccomp 프로파일은 Pod 수준과 container 수준 두 곳에서 지정합니다. Pod 수준에 두면 모든 컨테이너에 적용되고, container 수준에 두면 해당 컨테이너에만 적용되며 Pod 수준 설정을 덮어씁니다.

RuntimeDefault를 Pod 전체에 적용 #

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginx:1.27

spec.securityContext에 둔 seccompProfile은 이 Pod의 모든 컨테이너에 RuntimeDefault를 입힙니다. 대부분의 시험 작업은 이 형태로 끝납니다.

container 수준 적용 #

apiVersion: v1
kind: Pod
metadata:
  name: mixed-app
spec:
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      seccompProfile:
        type: RuntimeDefault
  - name: sidecar
    image: busybox:1.36
    command: ["sleep", "3600"]
    securityContext:
      seccompProfile:
        type: Localhost
        localhostProfile: profiles/audit.json

같은 Pod 안에서 컨테이너마다 다른 프로파일을 쓰는 예입니다. app은 런타임 기본값을, sidecar는 노드에 올려둔 커스텀 프로파일을 참조합니다. container 수준 설정이 Pod 수준 설정보다 우선합니다.

커스텀 프로파일 작성 #

런타임 기본값으로 부족할 때는 직접 JSON 프로파일을 작성합니다. 커스텀 프로파일은 노드의 정해진 디렉터리에 올려두어야 Localhost 타입으로 참조할 수 있습니다.

프로파일 디렉터리 #

kubelet은 커스텀 seccomp 프로파일을 노드의 다음 경로에서 찾습니다.

/var/lib/kubelet/seccomp/

localhostProfile에 적는 경로는 이 디렉터리를 기준으로 한 상대 경로입니다. 관례적으로 프로파일은 profiles/ 하위에 모아 둡니다. 예를 들어 파일을 다음 위치에 두면,

/var/lib/kubelet/seccomp/profiles/audit.json

매니페스트에서는 localhostProfile: profiles/audit.json으로 참조합니다. 절대 경로나 디렉터리 밖 경로는 허용되지 않습니다.

프로파일 JSON 구조 #

커스텀 프로파일의 핵심은 defaultActionsyscalls 두 필드입니다.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "accept4",
        "bind",
        "listen",
        "read",
        "write",
        "close",
        "exit_group"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

defaultActionSCMP_ACT_ERRNO이므로 나열되지 않은 모든 시스템 콜은 차단되고, 호출하면 오류(EPERM)를 돌려받습니다. syscalls 블록의 names에 올린 시스템 콜만 SCMP_ACT_ALLOW로 허용됩니다. 이 “기본 차단 + 명시 허용” 방식이 가장 안전한 화이트리스트 패턴입니다.

주요 액션값은 다음과 같습니다.

액션동작
SCMP_ACT_ERRNO호출 차단. 오류 코드 반환
SCMP_ACT_ALLOW호출 허용
SCMP_ACT_LOG허용하되 로그 기록(감사용)
SCMP_ACT_KILL호출 시 프로세스 종료

감사 목적의 프로파일은 defaultActionSCMP_ACT_LOG로 두어 어떤 시스템 콜이 쓰이는지 먼저 관찰한 뒤, 그 결과로 화이트리스트를 좁히는 방식으로 작성합니다.

커스텀 프로파일을 참조하는 Pod #

apiVersion: v1
kind: Pod
metadata:
  name: custom-seccomp
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/audit.json
  containers:
  - name: app
    image: nginx:1.27

typeLocalhost로 두고 localhostProfile에 디렉터리 기준 상대 경로를 적습니다. 해당 파일이 노드에 없으면 Pod는 생성 단계에서 오류가 납니다. 시험에서 커스텀 프로파일 작업이 나오면, 파일을 올바른 경로에 올렸는지부터 확인해야 합니다.

검증 #

적용한 seccomp이 실제로 시스템 콜을 막는지 확인하는 절차가 검증의 핵심입니다.

프로파일이 입혀졌는지 확인 #

kubectl get pod secure-app -o jsonpath='{.spec.securityContext.seccompProfile}'

Pod 스펙에 프로파일 타입이 들어갔는지 확인합니다. container 수준이라면 .spec.containers[0].securityContext.seccompProfile을 봅니다.

차단된 시스템 콜 테스트 #

defaultAction이 차단인 프로파일에서, 허용 목록에 없는 시스템 콜을 일부러 호출해 막히는지 봅니다. 예를 들어 mkdir을 허용하지 않은 프로파일이라면 디렉터리 생성이 실패해야 합니다.

kubectl exec custom-seccomp -- mkdir /tmp/test
mkdir: can't create directory '/tmp/test': Operation not permitted

Operation not permitted는 시스템 콜이 SCMP_ACT_ERRNO로 차단되었다는 신호입니다. 반대로 RuntimeDefault처럼 일반 동작을 허용하는 프로파일에서는 평범한 명령이 정상 동작해야 합니다. 이렇게 “막혀야 할 것이 막히고, 돌아가야 할 것이 도는지"를 양쪽으로 확인하면 작업이 끝납니다.

Unconfined로 떨어졌는지 점검 #

프로파일을 지정했다고 생각했는데 실제로는 Unconfined로 도는 경우가 잦은 실수입니다. Pod 스펙에 seccompProfile이 비어 있고 kubelet의 --seccomp-default도 꺼져 있으면, 컨테이너는 시스템 콜 제한 없이 뜹니다. 위 jsonpath 조회 결과가 비어 있다면 프로파일이 적용되지 않은 것이므로 매니페스트를 다시 봐야 합니다.

시험 포인트 #

  • RuntimeDefault가 기본 권장값입니다. “Pod에 seccomp을 적용하라"는 작업의 대부분은 securityContext.seccompProfile.type: RuntimeDefault 한 줄로 끝납니다.
  • 프로파일은 Pod 수준(spec.securityContext)과 container 수준(spec.containers[].securityContext) 두 곳에서 지정하며, container 수준이 우선합니다.
  • 커스텀 프로파일은 노드의 /var/lib/kubelet/seccomp/ 디렉터리에 올리고, localhostProfile에는 이 디렉터리 기준 상대 경로를 적습니다.
  • JSON 프로파일의 화이트리스트 패턴은 defaultAction: SCMP_ACT_ERRNO(기본 차단) + 허용 시스템 콜의 SCMP_ACT_ALLOW 조합입니다.
  • Unconfined는 seccomp 미적용이므로 피해야 할 값입니다. 프로파일을 지정하지 않으면 과거 기본값으로 떨어질 수 있다는 점을 기억합니다.
  • 노드 전체 강제는 kubelet의 --seccomp-default 플래그(또는 SeccompDefault 피처 게이트)로 처리합니다.
  • 검증은 kubectl get pod -o jsonpath로 적용 여부를 확인하고, 차단 대상 시스템 콜을 호출해 Operation not permitted가 나오는지로 마무리합니다.

정리 #

이번 글에서 잡은 것:

  • seccomp은 컨테이너가 커널에 던지는 시스템 콜을 필터링해 공격 표면을 좁히는 리눅스 커널 기능입니다.
  • 프로파일 타입은 RuntimeDefault(런타임 기본값,권장), Localhost(노드의 커스텀 파일 참조), Unconfined(미적용) 셋입니다.
  • 적용은 securityContext.seccompProfile로 하며 Pod 수준과 container 수준 모두 가능합니다.
  • 커스텀 프로파일은 /var/lib/kubelet/seccomp/에 올려 Localhost 타입으로 참조하고, defaultActionsyscalls로 허용 목록을 정의합니다.
  • seccomp이 시스템 콜을 본다면 AppArmor는 파일과 기능을 봅니다. 둘은 함께 쌓을 때 방어가 두터워집니다.

다음: kernel hardening #

seccomp으로 시스템 콜을, AppArmor로 파일과 기능을 묶었습니다. System Hardening도메인의 마지막 조각은 컨테이너에 넘기는 커널 권한 그 자체를 줄이는 일입니다.

#8 kernel hardening, capabilities, /proc 보호에서는 리눅스 capability를 최소로 떨어뜨리는 securityContext.capabilities, 권한 상승을 막는 allowPrivilegeEscalationprivileged, /proc 마스킹과 readOnlyRootFilesystem 같은 커널 수준의 하드닝 설정을 직접 만들어 보며 정리하겠습니다.

X