SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-21

VPS 단일 노드 k3s 클러스터 보안 설정 가이드

VPS에 설치된 k3s의 기본 보안 취약점을 해결합니다. 6443 및 10250 포트 노출, kubeconfig 권한, NodePort 방화벽 설정 및 권한 있는 파드 문제를 방지하는 구체적인 보안 강화 방법을 단계별로 설명합니다.

단일 노드 k3s 클러스터가 초기 상태에서 노출하는 항목

공용 VPS에 설치한 단일 노드 k3s 클러스터는 한 줄짜리 설치 스크립트가 완료된 직후 5곳이 외부로 노출됩니다. TCP 6443 포트의 Kubernetes API 서버, TCP 10250 포트의 kubelet, 디스크에 저장된 kubeconfig 파일, 방화벽이 감지하지 못하는 NodePort 범위, 그리고 privileged 또는 hostPath을 요청하도록 허용된 모든 파드입니다. 각 항목은 몇 분이면 해결할 수 있습니다. 이 가이드는 k3s가 이미 실행 중임을 가정하므로, 아직 설치되지 않았다면 VPS에 단일 노드 k3s 설치하기를 먼저 수행한 후 돌아오십시오.

설정을 변경하기 전에 무엇이 수신 대기 중인지 확인하십시오.

sudo ss -tulpn | grep -E '6443|10250|10256|8472'

기본 설치 시 6443(API 서버), 10250(kubelet), 10256(kube-proxy 상태 확인), 8472/udp(VXLAN을 사용하는 flannel 오버레이) 포트가 노출됩니다. k3s는 기본적으로 0.0.0.0에 바인딩되므로, 이 모든 포트가 루프백뿐만 아니라 공용 IP 주소에서도 수신 대기 상태가 됩니다.

포트 6443이 전체 클러스터인 이유

관리자 권한으로 포트 6443에 인증할 수 있는 모든 주체는 파드를 생성할 수 있으며, 파드는 호스트에서 root 권한을 가질 수 있습니다. 포트 6443은 해당 머신으로 들어가는 문입니다.

포트 6443이 열려 있다고 해서 즉시 침해당하는 것은 아닙니다. Kubernetes는 비밀번호를 허용하지 않기 때문입니다. 클라이언트 인증서나 베어러 토큰을 요구합니다. 하지만 다음 두 가지 사실은 여전히 유효합니다.

첫째, API 서버는 자격 증명이 없는 일부 요청에도 응답합니다. 기본 Kubernetes RBAC(역할 기반 접근 제어)는 system:unauthenticated 그룹을 system:public-info-viewer라는 역할에 바인딩하며, 이는 /version, /healthz, /livez/readyz을 허용합니다. 다른 머신에서 다음을 실행하면:

curl -sk https://YOUR_SERVER_IP:6443/version

정확한 Kubernetes 버전이 반환됩니다. 이는 CVE(공통 취약점 및 노출) 검색의 입력값이 되며, 스캐너가 귀하의 서버를 흥미로운 대상으로 판단하는 이유가 됩니다. 해당 경로 이후의 모든 요청은 거부되며, 거부 메시지에는 요청자의 정보가 포함됩니다:

forbidden: User "system:anonymous" cannot get path "/api"

둘째, API 서버의 모든 버그는 해당 포트가 열려 있는 동안 원격으로 접근 가능합니다. 이 시점부터 패치는 선택 사항이 아닙니다.

저렴한 해결책은 방화벽 규칙을 설정하는 것입니다. k3s는 클러스터 트래픽을 동일한 커널을 통해 라우팅하므로 ufw에는 한 줄 이상의 설정이 필요합니다.

sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow from 203.0.113.10 to any port 6443 proto tcp
sudo ufw allow from 10.42.0.0/16 to any
sudo ufw allow from 10.43.0.0/16 to any
sudo ufw enable

마지막 두 규칙은 k3s 문서에서 직접 가져온 것입니다. 10.42.0.0/16는 기본 파드 네트워크이고 10.43.0.0/16은 기본 서비스 네트워크입니다. 이 규칙이 없으면 ufw가 클러스터 내부 트래픽을 차단하여 파드가 API 서버 및 서로 간의 통신을 잃게 됩니다. k3s 예제는 모든 곳에서 6443 포트로의 접근을 허용하지만, 이를 귀하의 주소로 교체하는 것이 가치 있는 변경입니다. ufw가 처음이라면 VPS를 위한 ufw 방화벽 기초에서 이 설정이 의존하는 기본 정책을 다룹니다.

k3s 문서는 오버레이 포트에 대해 다음과 같이 명확히 경고합니다: "노드의 VXLAN 포트는 클러스터 네트워크를 누구에게나 노출할 수 있으므로 외부로 노출해서는 안 됩니다." 들어오는 트래픽에 대한 기본 거부(default deny) 정책을 사용하면 포트 번호를 일일이 지정하지 않아도 이를 처리할 수 있습니다.

더 강력한 해결책은 공용 주소를 통해 API에 접근하는 것을 완전히 중단하고 VPN이나 메시 주소를 사용하는 것입니다. 서버 인증서에는 연결할 주소가 나열되어 있어야 하므로, /etc/rancher/k3s/config.yaml에 SAN(주체 대체 이름)으로 추가하십시오:

tls-san:
  - 10.8.0.1
  - k3s.example.com
secrets-encryption: true
sudo systemctl restart k3s
sudo k3s secrets-encrypt status

secrets-encryption: true는 데이터 저장소의 Secret 객체를 암호화합니다. k3s 문서는 "Secrets-encryption은 서버를 재시작하지 않으면 기존 서버에서 활성화할 수 없으며", 변경 전에 작성된 Secret은 sudo k3s secrets-encrypt reencrypt을 실행하기 전까지 이전 형식을 유지한다고 명시합니다. 이 설정이 무엇을 보장하는지 명확히 이해해야 합니다. 이는 백업에서 복사된 데이터 저장소 파일을 보호합니다. API 서버와 통신할 수 있는 공격자에게는 아무런 효과가 없습니다. API 서버는 Secret을 읽을 권한이 있는 사람에게는 이를 복호화하여 제공하기 때문입니다. 이러한 구분은 모든 자체 호스팅 Secret 저장소에 동일하게 적용되며, 이것이 바로 Vaultwarden 보안 강화에서 암호화 자체보다 관리자 토큰과 백업 파일에 시간을 할애하는 이유입니다.

왜 10250 포트의 kubelet이 중요한가

kubelet은 컨테이너를 시작하는 에이전트입니다. 10250 포트에서 동작하는 kubelet API는 파드 목록을 나열하고 그 안에서 명령을 실행할 수 있게 합니다. 익명 요청을 허용하는 kubelet은 해당 머신에서 실행 중인 모든 워크로드에 대한 원격 셸(remote shell)과 같습니다.

본인의 설정을 확인하십시오:

curl -sk https://127.0.0.1:10250/pods | head -c 60

최신 k3s는 Unauthorized라고 응답합니다. kubelet이 모든 호출자에 대해 API 서버에 인증 및 권한 부여를 요청하기 때문입니다. 만약 JSON 형식의 파드 목록이 반환된다면 익명 접근이 활성화된 상태이며, 10250 포트에 접근할 수 있는 누구나 컨테이너 정보를 읽거나 명령을 실행할 수 있습니다.

어떤 경우든 외부에서의 접근을 차단하십시오. 단일 노드 환경에서 kubelet의 유일한 클라이언트는 같은 장비 내의 컨트롤 플레인이며, 해당 트래픽은 ufw가 기본적으로 허용하는 루프백 인터페이스를 통해 들어옵니다. 인터넷에서 10250 포트로의 접근을 거부해도 운영상 지장은 없습니다. 만약 kubectl top 오류가 발생하거나 metrics-server가 정상적으로 동작하지 않아 이 문서를 보게 되었다면, 그 원인들은 kubelet 10250 포트 오류에서 확인할 수 있습니다.

kubeconfig는 클러스터 관리자 자격 증명입니다

k3s는 /etc/rancher/k3s/k3s.yaml을(를) root 소유, 모드 600으로 생성합니다. 이 권한을 변경할 때 발생하는 결과에 대해 공식 문서에서는 다음과 같이 설명합니다: "kubeconfig 파일은 root가 소유하며 기본 모드는 600입니다. 모드를 644로 변경하면 호스트의 다른 권한 없는 사용자도 파일을 읽을 수 있게 됩니다."

이를 다르게 해석하면, 모드 644는 모든 로컬 계정을 클러스터 관리자로 만드는 것과 같습니다. 많은 가이드에서 --write-kubeconfig-mode 644을(를) 사용하여 sudo 없이 kubectl이(가) 작동하도록 설정하라고 권장하지만, 이는 셸 접근 권한이 있는 누구에게나 관리자 자격 증명을 넘겨주는 위험한 방식입니다.

대신 파일을 특정 사용자에게 복사하십시오.

mkdir -p ~/.kube
sudo install -o "$USER" -g "$USER" -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config
kubectl get nodes

그런 다음 원본 파일의 권한이 여전히 엄격하게 유지되는지 확인하십시오:

stat -c '%a %U:%G' /etc/rancher/k3s/k3s.yaml

원하는 답은 600 root:root입니다. 해당 파일에는 system:masters 그룹의 구성원을 위한 클라이언트 인증서가 포함되어 있습니다. API 서버는 이 그룹을 무조건 허용하므로 RBAC 규칙을 확인하지 않습니다. Kubernetes에는 인증서 폐기 목록(CRL)이 없으므로, 유출된 사본은 클러스터 인증 기관(CA)을 교체하기 전까지 계속 유효합니다. 이 파일을 SSH 개인 키처럼 취급하고 접근 가능한 계정을 최소한으로 유지하십시오. 이는 VPS에서 최소 권한 사용자 계정 사용과 같은 맥락의 논리입니다.

방화벽이 NodePort 트래픽을 감지하지 못하는 경우

type: NodePort 서비스는 노드가 보유한 모든 주소(공용 주소 포함)에서 30000번부터 32767번 사이의 포트를 엽니다. k3s의 type: LoadBalancer 서비스는 한 단계 더 나아갑니다. 번들로 제공되는 로드 밸런서인 ServiceLB는 kube-system 내에 서비스당 작은 파드 하나를 스케줄링하여 호스트의 서비스 포트를 직접 점유합니다.

kubectl -n kube-system get pods | grep svclb
sudo ss -tulpn | grep -E ':3[0-2][0-9]{3}'

이제 사람들이 의아해하는 부분입니다. ufw로 해당 포트를 차단해도 응답은 계속됩니다.

sudo ufw deny 30080/tcp
curl http://YOUR_SERVER_IP:30080

패킷이 이동하는 경로 때문에 페이지는 여전히 로드됩니다. kube-proxy는 nat 테이블의 PREROUTING 체인에 DNAT(목적지 네트워크 주소 변환) 규칙을 작성하며, PREROUTING은 필터링 결정이 내려지기 전에 실행됩니다. 목적지가 호스트가 아닌 파드 주소로 변경되므로 커널은 패킷을 FORWARD 체인으로 보내며 INPUT 체인을 거치지 않습니다. ufw 규칙은 INPUT에 존재하므로 패킷은 이 규칙들을 만나지 않습니다. 점프 순서를 확인하면 다음과 같습니다.

sudo iptables -S PREROUTING -t nat | head
sudo iptables -S FORWARD | head

KUBE-SERVICES은 PREROUTING의 최상단에 위치하며, Kubernetes의 FORWARD 점프는 ufw 자체 체인보다 위에 있습니다. 이는 Docker가 ufw를 우회하여 포트를 게시하게 만드는 것과 동일한 메커니즘이며, 해결책 또한 같습니다.

  • 제공업체의 네트워크 방화벽에서 필터링하십시오. 이는 장비 앞단에서 실행되며 커널의 라우팅 방식에 영향을 받지 않습니다.
  • NodePort와 LoadBalancer 사용을 피하십시오. 서비스를 ClusterIP로 유지하고 이미 연결된 SSH 세션을 통해 kubectl port-forward로 접근하십시오.
  • 80번과 443번 포트로 Ingress 하나만 노출하고 나머지는 노출하지 마십시오.
  • kube-apiserver-arg 아래의 service-node-port-range 설정을 통해 범위를 좁히십시오. 그러면 실수로 NodePort가 열리더라도 관리자가 감시하는 범위 내에 위치하게 됩니다.

ufw는 여전히 유용합니다. ufw는 SSH나 API 서버와 같이 호스트 자체를 대상으로 하는 트래픽을 제어합니다. 단지 파드 트래픽을 감시하지 않을 뿐이며, ufw가 이를 제어할 것이라고 기대하는 것이 데이터베이스가 외부로 노출되는 원인이 됩니다.

hostPath 또는 privileged를 사용하는 파드는 VPS의 root 권한을 가집니다

컨테이너는 커널의 제한된 뷰를 가진 일반적인 프로세스입니다. 몇몇 파드 필드는 이러한 제한을 해제합니다.

  • securityContext.privileged: true은 컨테이너에 모든 Linux 기능과 호스트 장치에 대한 접근 권한을 부여합니다.
  • hostPath는 호스트 디렉터리를 파드 내부에 마운트합니다. /을 읽기-쓰기 권한으로 마운트한 파드는 /root/.ssh/authorized_keys에 키를 추가할 수 있습니다.
  • hostPID: true는 컨테이너를 호스트 프로세스 네임스페이스에 배치하며, 여기서 PID 1을 대상으로 nsenter을 실행하면 호스트 셸이 열립니다.
  • hostNetwork: true은 컨테이너를 호스트 네트워크 스택에 배치하며, 여기서 호스트 포트를 바인딩하고 루프백에 바인딩된 서비스에 접근할 수 있습니다.

따라서 "누가 여기서 파드를 생성할 수 있는가"라는 질문은 "누가 이 VPS의 root인가"라는 질문과 같습니다. 파드에 대해 create 권한을 가진 모든 ServiceAccount는 파드를 거부하는 별도의 장치가 없는 한 root와 동일한 권한을 가집니다.

그 장치가 바로 API 서버에 내장된 Pod Security admission입니다. 간단히 말해 네임스페이스별로 라벨을 지정하는 방식이며 재시작이 필요하지 않습니다.

kubectl label namespace default \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/warn=restricted

baseline는 위에서 언급한 네 가지 필드를 모두 거부합니다. restricted은 한 걸음 더 나아가 non-root 사용자, seccomp(secure computing mode) 프로필, 권한 상승 금지, 그리고 ALL로 제한된 기능을 요구하며, 이는 많은 공개된 차트들을 작동하지 않게 만듭니다. baseline를 강제(enforce)하면서 restricted에 대해서는 경고(warn)만 설정하면, 설정을 적용하기 전에 무엇이 중단될지 미리 확인할 수 있습니다.

작동 여부를 확인합니다:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: pstest
spec:
  containers:
  - name: app
    image: busybox
    command: ["sleep", "60"]
    securityContext:
      privileged: true
EOF

API 서버는 이를 거부하며, 거부된 필드가 무엇인지 알려줍니다:

Error from server (Forbidden): error when creating "STDIN": pods "pstest" is forbidden: violates PodSecurity "baseline:latest": privileged (container "app" must not set securityContext.privileged=true)

각 네임스페이스에 라벨을 붙이는 대신 클러스터 전체에 기본값을 적용하려면, k3s는 /var/lib/rancher/k3s/server/psa.yaml에 있는 admission 설정 파일을 사용하도록 안내합니다:

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
  configuration:
    apiVersion: pod-security.admission.config.k8s.io/v1beta1
    kind: PodSecurityConfiguration
    defaults:
      enforce: "baseline"
      enforce-version: "latest"
      warn: "restricted"
      warn-version: "latest"
    exemptions:
      namespaces: [kube-system]

/etc/rancher/k3s/config.yaml에서 API 서버가 해당 파일을 참조하도록 설정한 뒤 k3s를 재시작합니다:

kube-apiserver-arg:
  - 'admission-control-config-file=/var/lib/rancher/k3s/server/psa.yaml'

kube-system 예외 설정은 선택 사항이 아닙니다. k3s 자체의 ServiceLB 파드는 호스트 포트를 점유하는데, baseline은 이를 금지하므로 kube-system을 목록에서 제외하면 해당 파드들이 재생성될 때 거부됩니다. admission 설정을 변경한 후 k3s를 재시작할 때는 반드시 두 번째 SSH 세션을 열어두십시오.

참고로, k3s는 네트워크 정책 컨트롤러를 기본적으로 포함하고 활성화하므로, 별도의 설치 없이도 이 클러스터에서 NetworkPolicy 객체가 즉시 적용됩니다. 이는 모든 Kubernetes 배포판에서 지원되는 기능은 아니며, 침해된 파드가 다른 파드에 접근하지 못하도록 차단하는 도구입니다.

사용하지 않는 번들 구성 요소 제거

설치 프로그램은 일련의 애드온을 함께 배포합니다. 각 애드온은 리스너를 추가하고 패치해야 할 대상을 하나 더 늘립니다. --disablecoredns, servicelb, traefik, local-storage, metrics-server, runtimes 값을 허용합니다.

coredns은 유지하십시오. 클러스터 내의 어떤 것도 이것 없이는 이름을 해석할 수 없습니다. 나머지는 선택 사항입니다. /etc/rancher/k3s/config.yaml에서 설정합니다:

disable:
  - traefik
  - servicelb
disable-helm-controller: true
sudo systemctl restart k3s
kubectl get pods -A

k3s는 비활성화한 구성 요소를 삭제하므로 traefik 파드와 svclb- 파드는 자동으로 사라집니다. 먼저 그 결과를 숙지하십시오. servicelb이 제거되면 모든 type: LoadBalancer 서비스는 주소를 할당받을 수 없으므로 영원히 <pending> 상태로 머뭅니다. traefik이 제거되면 인그레스 컨트롤러가 없으므로 Ingress 객체는 아무런 동작도 하지 않습니다. 호스트의 리버스 프록시와 같이 다른 방식으로 트래픽을 처리할 때만 비활성화하고, 사용하는 경우에는 그대로 두십시오. disable-helm-controller: trueHelmChart 리소스를 감시하는 컨트롤러를 제거합니다. 이는 직접 helm을 실행하는 경우 사용하지 않는 권한 있는 구성 요소입니다.

기본 ServiceAccount 토큰 자동 마운트 중지

별도의 설정을 하지 않으면 모든 파드는 /var/run/secrets/kubernetes.io/serviceaccount/token에 ServiceAccount 토큰을 할당받습니다. default ServiceAccount는 어떠한 RBAC 권한도 가지고 있지 않으므로, 토큰 자체만으로는 큰 위협이 되지 않습니다. 하지만 침해된 컨테이너 내부의 공격자에게는 유효한 자격 증명과 접근 가능한 API 서버를 제공하게 되며, 이는 대부분의 클러스터 권한 상승 시나리오의 첫 단계가 됩니다.

k3s 보안 강화 가이드는 네임스페이스별로 이 기능을 비활성화하도록 권장합니다.

kubectl patch serviceaccount --namespace default default --patch '{"automountServiceAccountToken": false}'
kubectl patch serviceaccount --namespace kube-node-lease default --patch '{"automountServiceAccountToken": false}'
kubectl patch serviceaccount --namespace kube-public default --patch '{"automountServiceAccountToken": false}'

설정이 적용되었는지 확인합니다. 파드가 시작될 때까지 몇 초 정도 기다려 주십시오.

kubectl run t --image=busybox --restart=Never --command -- sleep 30
kubectl exec t -- ls /var/run/secrets/kubernetes.io/serviceaccount

경로가 제거되었으므로 lsNo such file or directory을 출력합니다. API 접근이 반드시 필요한 워크로드는 자체 파드 명세에 automountServiceAccountToken: true를 설정하면 되므로, 어떤 기능도 영구적으로 차단되지 않습니다. 이는 그 자체로 작은 보안 향상입니다. 더 큰 이점은 실제 권한이 있는 ServiceAccount를 워크로드에 부여하지 않는 것이며, 현재 토큰의 가치는 다음 명령으로 확인할 수 있습니다.

kubectl auth can-i --list --as=system:serviceaccount:default:default

리소스 제한을 통한 클러스터 보호

단일 노드 환경에서는 컨트롤 플레인과 워크로드가 하나의 커널과 메모리 풀을 공유합니다. 메모리 누수가 발생하는 파드는 혼자 종료되지 않을 수 있습니다. 커널의 OOM(out of memory) 킬러는 큰 프로세스에 높은 점수를 부여하여 희생양을 선택하는데, k3s는 오랫동안 실행되는 대규모 프로세스이므로 파드 대신 클러스터 자체가 사라질 수 있습니다. 워크로드를 재시작하는 주체인 k3s가 죽었기 때문에 워크로드는 다시 시작되지 않습니다.

LimitRange를 사용하면 제한을 설정하지 않은 파드에 기본값을 적용할 수 있습니다.

apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
  namespace: default
spec:
  limits:
  - type: Container
    default:
      cpu: 500m
      memory: 512Mi
    defaultRequest:
      cpu: 50m
      memory: 128Mi

ResourceQuota는 전체 네임스페이스가 점유할 수 있는 자원 총량을 제한합니다.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: cap
  namespace: default
spec:
  hard:
    limits.cpu: "3"
    limits.memory: 3Gi
    pods: "20"

그런 다음 /etc/rancher/k3s/config.yaml에서 k3s를 위한 자원을 확보하십시오.

kubelet-arg:
  - 'system-reserved=cpu=250m,memory=512Mi'

이 차이는 두 곳에서 나타납니다. 자체 제한을 초과하여 종료된 컨테이너는 kubectl describe podLast State 항목 아래에 Reason: OOMKilled를 보고하며, 노드의 나머지 부분은 정상적으로 작동합니다. 메모리가 완전히 고갈된 노드는 dmesgKilled process 라인을 남기며, 대개 주변 프로세스까지 함께 종료됩니다. 전자는 제한 설정이 의도대로 작동한 경우이며, 후자는 제한 설정을 통해 방지하고자 하는 상황입니다.

CI 및 k3s 클러스터에서 주기적으로 매니페스트 스캔하기

스캔 작업은 두 곳에서 수행해야 하며, 각기 다른 문제를 탐지합니다. 먼저 Trivy를 설치하십시오. 이 프로젝트는 모든 릴리스마다 Debian 패키지를 제공합니다. 2026년 8월 기준 최신 버전은 0.74.0이었으므로, 자동화 스크립트에 고정하기 전에 릴리스 페이지에서 더 최신 버전이 있는지 확인하십시오.

sudo apt-get install -y wget
wget -q https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-64bit.deb
sudo dpkg -i trivy_0.74.0_Linux-64bit.deb
trivy --version

첫 번째 장소는 매니페스트가 클러스터에 도달하기 전인 CI 단계입니다.

trivy fs --scanners misconfig --severity HIGH,CRITICAL --exit-code 1 ./deploy

--exit-code 1는 탐지된 결과가 있을 경우 CI(지속적 통합) 작업을 실패 처리합니다. 각 결과는 실패 원인이 된 필드와 심각도를 명시하므로, 의도하지 않은 privileged: true이 포함된 경우 API 서버에 도달하기 전에 빌드를 실패시킵니다. 수용하기로 결정한 탐지 결과는 .trivyignore 파일에 기록하며, 이 파일은 해당 매니페스트와 함께 git에 저장되어 결정 근거를 유지합니다.

두 번째 장소는 실행 중인 클러스터이며, 주기적으로 스캔을 수행합니다.

trivy k8s --compliance=k8s-cis-1.23 --report summary

해당 명령어에 대해 솔직히 말씀드릴 점이 있습니다. trivy k8s는 노드 수준의 설정을 검사하기 위해 호스트 접근 권한이 필요한 노드 수집기 파드를 배포합니다. 권한이 있는(privileged) 파드를 막기 시작한 클러스터라면, 이를 우회하기보다 이러한 특성을 인지하는 것이 중요합니다. trivy k8s --report summary --disable-node-collector 옵션을 사용하면 수집기를 건너뛰게 되며, 이로 인해 노드 수준의 검사 기능도 함께 상실됩니다.

kube-bench는 호스트 측면을 다룹니다. CIS(Center for Internet Security) 벤치마크의 대부분을 차지하는 파일 권한 및 프로세스 플래그를 검사합니다. 이 도구는 k3s 프로파일을 제공하므로, 업스트림 Job 매니페스트를 가져와 조정하십시오.

curl -sfL -o kube-bench-job.yaml https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml

컨테이너 명령어를 ["kube-bench", "--benchmark", "k3s-cis-1.7"]로 변경하고, /etc/kubernetes/var/lib/etcd 마운트를 /etc/rancher/var/lib/rancher로 교체하십시오. k3s는 해당 경로에 파일을 보관하기 때문입니다. 그 후 다음을 실행합니다.

kubectl apply -f kube-bench-job.yaml
kubectl logs -l app=kube-bench --tail=-1

이 Job이 무엇인지 확인하십시오. hostPID: true에 호스트 디렉터리 마운트가 추가된 형태이며, 이는 바로 앞 섹션에서 차단하기 시작한 파드 형태와 정확히 일치합니다. 이를 예외 처리된 kube-system 네임스페이스에서 실행하고, 결과를 읽은 뒤 kubectl delete job kube-bench 하십시오. 권한이 필요한 스캐너를 사용한다고 해서 권한이 있는 워크로드를 금지하는 정책을 멈출 이유는 없습니다.

이미지 내부 콘텐츠는 별개의 문제입니다. trivy image ghcr.io/example/app:1.4는 이미지 내부의 패키지 데이터베이스를 읽어 알려진 취약점 목록을 출력합니다. 이는 서버의 알려진 CVE를 점검하는 것과 동일한 작업입니다.

이제 스캐너 출력 결과에 대해 솔직하게 말씀드리겠습니다. 단일 노드 취미용 클러스터는 CIS 통제 항목에서 긴 실패 목록을 보여줄 것이며, 그중 대부분은 사실이지만 귀하에게는 무관한 항목입니다. 이 벤치마크는 다중 노드, 다중 테넌트 클러스터를 위해 작성되었습니다. 별도 호스트의 etcd, 외부로 전송되는 감사 로그, 별도의 kubelet 인증 기관, 귀하에게 적용되지 않는 규정 준수 체계를 위한 어드미션 플러그인 등이 포함되어 있기 때문입니다. k3s는 제어 평면을 단일 설정 파일로 구성된 단일 프로세스로 실행하도록 설계되었으므로, kube-scheduler 매니페스트 파일의 권한을 확인하는 통제 항목은 해당 파일이 존재하지 않아 통과할 수 없습니다.

실패 항목을 다음 순서대로 읽고 가치가 없다고 판단되면 중단하십시오. /etc/rancher/var/lib/rancher 하위의 파일 모드와 소유권, 익명 또는 인증되지 않은 접근 관련 항목, 0.0.0.0에 바인딩된 컴포넌트 보고, 그리고 특별한 이유 없이 UID 0으로 실행되는 컨테이너가 우선순위입니다. 나머지는 두 번째 노드가 생기거나 접근 권한을 가진 사람이 추가될 때까지 미뤄두어도 됩니다. 무시하게 되는 100줄짜리 보고서보다, 즉시 조치할 수 있는 5줄짜리 보고서가 훨씬 가치 있습니다.

단일 노드 k3s 강화 절차

  1. ufw 기본 수신 정책을 deny로 설정하고, SSH를 허용하며, 본인의 주소에서 6443 포트로의 접근을 허용하고, pod 및 service 네트워크를 허용합니다.
  2. kubeconfig를 사용자 계정으로 복사하고 권한을 600으로 설정하며, --write-kubeconfig-mode 644은 절대 설정하지 마십시오.
  3. 10250 포트의 kubelet이 익명 요청을 거부하는지 확인하고, 해당 포트를 인터넷에 노출하지 마십시오.
  4. 사용하지 않는 번들 구성 요소를 비활성화한 뒤 k3s를 재시작합니다.
  5. enforce=baselinewarn=restricted를 사용하여 Pod Security admission을 위한 네임스페이스 레이블을 지정합니다.
  6. 기본 ServiceAccount 토큰 자동 마운트를 비활성화합니다.
  7. LimitRange와 ResourceQuota를 추가하고, k3s를 위한 CPU와 메모리를 예약합니다.
  8. CI에 trivy fs --scanners misconfig을 포함하고, 매달 CIS 스캔을 실행합니다.

기반 호스트는 다른 서버와 마찬가지로 동일한 관리가 필요하며, k3s는 여기에 복잡성을 더합니다. k3s는 apt가 아닌 스크립트로 설치되었으므로 apt upgrade이 이를 관리하지 않습니다. Ubuntu의 unattended upgrades를 사용하여 운영 체제 패치를 별도의 일정으로 유지하고, 원하는 채널이나 버전으로 설치 프로그램을 다시 실행하여 k3s를 신중하게 업그레이드하십시오. 한 대의 머신에서 두 가지 업데이트 경로를 관리하다 보면 잊기 쉬우므로, 어떤 구성 요소가 어떤 주기를 따르는지 기록해 두십시오.

FAQ

k3s API 서버를 6443 포트로 인터넷에 노출해도 안전합니까?

API 서버는 클라이언트 인증서나 토큰을 요구하며 그 외의 모든 요청은 forbidden: User "system:anonymous"로 거부하므로, 그 자체로 열린 문은 아닙니다. 하지만 두 가지 위험이 남습니다. 익명의 호출자가 /version을 읽을 수 있는데, 이는 스캐너에게 어떤 Kubernetes 릴리스를 공격 대상으로 삼아야 할지 정확히 알려줍니다. 또한 포트가 열려 있는 동안 향후 발견될 모든 API 서버 취약점에 원격으로 접근할 수 있게 됩니다. 단일 노드 클러스터라면 사용자 본인의 kubectl 외에는 6443 포트에 접근할 외부 대상이 없으므로, sudo ufw allow from YOUR_IP to any port 6443 proto tcp를 사용하여 본인의 주소에서만 접근을 허용하고 나머지는 기본 거부 정책으로 처리하십시오.

ufw 규칙이 NodePort 서비스를 차단하지 못하는 이유는 무엇입니까?

패킷이 규칙이 위치한 체인에 도달하지 않기 때문입니다. kube-proxy는 nat 테이블의 PREROUTING 체인에 DNAT 규칙을 삽입하는데, 이 체인은 가장 먼저 실행되어 목적지를 파드 주소로 재작성합니다. 패킷은 로컬로 전달되는 대신 포워딩되므로 INPUT 체인을 건너뛰고 FORWARD 체인을 통과하게 되며, ufw 규칙은 INPUT 체인에 존재합니다. sudo iptables -S PREROUTING -t nat | head 명령으로 이를 확인하십시오. NodePort는 제공업체의 네트워크 방화벽에서 필터링하거나, type: NodePort 사용을 피하고 대신 kubectl port-forward를 통해 서비스에 접근하십시오.

k3s를 --write-kubeconfig-mode 644 옵션으로 실행해야 합니까?

아니요. k3s 문서에는 "모드를 644로 변경하면 호스트의 다른 권한 없는 사용자가 파일을 읽을 수 있게 됩니다"라고 명시되어 있습니다. 해당 파일은 system:masters을 위한 클라이언트 인증서를 포함하고 있으므로, 644 모드로 설정하면 로컬의 모든 계정이 클러스터 관리자 권한을 갖게 됩니다. 대신 sudo install -o "$USER" -g "$USER" -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config를 사용하여 특정 사용자에게만 복사하고, 원본은 600 root:root에 그대로 두십시오.

k3s에는 어떤 kube-bench 벤치마크를 사용해야 합니까?

k3s-cis-1.7를 사용하십시오. kube-bench 문서에는 "kube-bench는 Rancher K3S 플랫폼을 위한 벤치마크를 포함합니다. 이를 실행하려면 kube-bench 명령 실행 시 --benchmark k3s-cis-1.7를 지정해야 합니다"라고 명시되어 있습니다. 자동 감지 기능은 kubeadm 레이아웃을 가정하지만 k3s는 파일을 /etc/rancher/var/lib/rancher 아래에 보관하므로 명시적으로 전달해야 합니다. 단일 노드에 적용되지 않는 실패 항목이 발생할 수 있으니, 파일 권한과 익명 접근 문제부터 우선적으로 조치하십시오.

Pod Security admission이 k3s 번들 구성 요소를 중단시킵니까?

kube-system에 강제 적용하면 중단될 수 있습니다. k3s가 type: LoadBalancer 서비스를 위해 생성하는 ServiceLB 파드는 호스트 포트를 점유하는데, baseline은 호스트 포트 사용을 금지하므로 해당 파드가 재생성될 때 거부됩니다. admission 설정 파일에서 kube-system을 예외 처리하거나, 본인이 소유한 네임스페이스에만 레이블 형태로 Pod Security를 적용하십시오. enforce=baselinewarn=restricted으로 시작하여, 기능을 활성화하기 전에 restricted가 무엇을 중단시킬지 먼저 확인하십시오.