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 도구이지만 막는 층위가 다릅니다.
| 항목 | seccomp | AppArmor |
|---|---|---|
| 대상 | 시스템 콜(syscall) | 파일 경로, capability, 네트워크 |
| 질문 | “이 시스템 콜을 허용할까” | “이 파일을 읽거나 쓸까” |
| 정의 위치 | JSON 프로파일 | 텍스트 프로파일(/etc/apparmor.d/) |
| 적용 키 | securityContext.seccompProfile | annotation 또는 securityContext.appArmorProfile |
| 적용 단위 | Pod 또는 container | container |
둘은 경쟁 관계가 아니라 보완 관계입니다. seccomp으로 위험한 시스템 콜을 막고, AppArmor로 파일과 기능 접근을 묶으면 방어가 겹겹이 쌓입니다. 시험에서는 두 도구를 각각 다루지만, 실무에서는 함께 적용하는 것이 정석입니다.
세 가지 프로파일 타입 #
쿠버네티스의 seccompProfile.type은 세 가지 값을 가집니다.
| type | 의미 | 비고 |
|---|---|---|
RuntimeDefault | 컨테이너 런타임이 제공하는 기본 프로파일 적용 | containerd,CRI-O가 검증한 합리적 기본값. 권장 |
Localhost | 노드에 올려둔 커스텀 프로파일 파일 참조 | localhostProfile로 파일 경로 지정 |
Unconfined | seccomp 미적용. 모든 시스템 콜 허용 | 사실상 무방비. 피해야 함 |
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.27spec.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 구조 #
커스텀 프로파일의 핵심은 defaultAction과 syscalls 두 필드입니다.
{
"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"
}
]
}defaultAction이 SCMP_ACT_ERRNO이므로 나열되지 않은 모든 시스템 콜은 차단되고, 호출하면 오류(EPERM)를 돌려받습니다. syscalls 블록의 names에 올린 시스템 콜만 SCMP_ACT_ALLOW로 허용됩니다. 이 “기본 차단 + 명시 허용” 방식이 가장 안전한 화이트리스트 패턴입니다.
주요 액션값은 다음과 같습니다.
| 액션 | 동작 |
|---|---|
SCMP_ACT_ERRNO | 호출 차단. 오류 코드 반환 |
SCMP_ACT_ALLOW | 호출 허용 |
SCMP_ACT_LOG | 허용하되 로그 기록(감사용) |
SCMP_ACT_KILL | 호출 시 프로세스 종료 |
감사 목적의 프로파일은 defaultAction을 SCMP_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.27type을 Localhost로 두고 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/testmkdir: can't create directory '/tmp/test': Operation not permittedOperation 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타입으로 참조하고,defaultAction과syscalls로 허용 목록을 정의합니다. - seccomp이 시스템 콜을 본다면 AppArmor는 파일과 기능을 봅니다. 둘은 함께 쌓을 때 방어가 두터워집니다.
다음: kernel hardening #
seccomp으로 시스템 콜을, AppArmor로 파일과 기능을 묶었습니다. System Hardening도메인의 마지막 조각은 컨테이너에 넘기는 커널 권한 그 자체를 줄이는 일입니다.
#8 kernel hardening, capabilities, /proc 보호에서는 리눅스 capability를 최소로 떨어뜨리는 securityContext.capabilities, 권한 상승을 막는 allowPrivilegeEscalation과 privileged, /proc 마스킹과 readOnlyRootFilesystem 같은 커널 수준의 하드닝 설정을 직접 만들어 보며 정리하겠습니다.