Seccomp trong Kubernetes: 7 điều bạn cần biết ngay từ đầu

Ghi chú. bản dịch.: Chúng tôi xin giới thiệu với các bạn bản dịch bài viết của một kỹ sư bảo mật ứng dụng cấp cao tại công ty ASOS.com của Anh. Với nó, anh bắt đầu một loạt ấn phẩm nhằm cải thiện tính bảo mật trong Kubernetes thông qua việc sử dụng seccomp. Nếu độc giả thích phần giới thiệu, chúng tôi sẽ theo dõi tác giả và tiếp tục với những tài liệu sau này của ông về chủ đề này.

Seccomp trong Kubernetes: 7 điều bạn cần biết ngay từ đầu

Bài viết này là bài viết đầu tiên trong loạt bài viết về cách tạo hồ sơ seccomp theo tinh thần SecDevOps mà không cần dùng đến phép thuật và phép thuật phù thủy. Trong Phần XNUMX, tôi sẽ trình bày những kiến ​​thức cơ bản và chi tiết nội bộ về việc triển khai seccomp trong Kubernetes.

Hệ sinh thái Kubernetes cung cấp nhiều cách khác nhau để bảo mật và cách ly các container. Bài viết nói về Chế độ tính toán an toàn hay còn gọi là bí mật. Bản chất của nó là lọc các lệnh gọi hệ thống có sẵn để thực thi bằng các vùng chứa.

Tại sao nó lại quan trọng? Một container chỉ là một tiến trình chạy trên một máy cụ thể. Và nó sử dụng kernel giống như các ứng dụng khác. Nếu vùng chứa có thể thực hiện bất kỳ lệnh gọi hệ thống nào, phần mềm độc hại sẽ sớm lợi dụng điều này để vượt qua sự cô lập vùng chứa và ảnh hưởng đến các ứng dụng khác: chặn thông tin, thay đổi cài đặt hệ thống, v.v.

Cấu hình seccomp xác định cuộc gọi hệ thống nào sẽ được cho phép hoặc vô hiệu hóa. Thời gian chạy vùng chứa kích hoạt chúng khi nó khởi động để hạt nhân có thể giám sát quá trình thực thi của chúng. Việc sử dụng các cấu hình như vậy cho phép bạn hạn chế vectơ tấn công và giảm thiệt hại nếu bất kỳ chương trình nào bên trong vùng chứa (nghĩa là phần phụ thuộc của bạn hoặc phần phụ thuộc của chúng) bắt đầu thực hiện điều gì đó mà nó không được phép làm.

Đi đến những điều cơ bản

Cấu hình seccomp cơ bản bao gồm ba thành phần: defaultAction, architectures (hoặc archMap) Và syscalls:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(trung bình-cơ bản-seccomp.json)

defaultAction xác định số phận mặc định của bất kỳ cuộc gọi hệ thống nào không được chỉ định trong phần syscalls. Để làm cho mọi việc dễ dàng hơn, hãy tập trung vào hai giá trị chính sẽ được sử dụng:

  • SCMP_ACT_ERRNO — chặn việc thực hiện cuộc gọi hệ thống,
  • SCMP_ACT_ALLOW - cho phép.

Trong phần architectures kiến trúc mục tiêu được liệt kê. Điều này rất quan trọng vì bản thân bộ lọc, được áp dụng ở cấp kernel, phụ thuộc vào số nhận dạng cuộc gọi hệ thống chứ không phụ thuộc vào tên của chúng được chỉ định trong cấu hình. Thời gian chạy vùng chứa sẽ khớp chúng với số nhận dạng trước khi sử dụng. Ý tưởng là các cuộc gọi hệ thống có thể có các ID hoàn toàn khác nhau tùy thuộc vào kiến ​​trúc hệ thống. Ví dụ: cuộc gọi hệ thống recvfrom (dùng để nhận thông tin từ socket) có ID = 64 trên hệ thống x64 và ID = 517 trên x86. Здесь bạn có thể tìm thấy danh sách tất cả các lệnh gọi hệ thống dành cho kiến ​​trúc x86-x64.

Trong phần syscalls liệt kê tất cả các cuộc gọi hệ thống và chỉ định những việc cần làm với chúng. Ví dụ: bạn có thể tạo danh sách trắng bằng cách cài đặt defaultAction trên SCMP_ACT_ERRNO, và các cuộc gọi trong phần syscalls giao phó SCMP_ACT_ALLOW. Vì vậy, bạn chỉ cho phép các cuộc gọi được chỉ định trong phần syscalls, và cấm tất cả những thứ khác. Đối với danh sách đen bạn nên thay đổi giá trị defaultAction và hành động ngược lại.

Bây giờ chúng ta nên nói một vài lời về những sắc thái không quá rõ ràng. Xin lưu ý rằng các đề xuất bên dưới giả định rằng bạn đang triển khai một dòng ứng dụng kinh doanh trên Kubernetes và bạn muốn chúng chạy với ít đặc quyền nhất có thể.

1. AllowPrivilegeEscalation=false

В securityContext vùng chứa có một tham số AllowPrivilegeEscalation. Nếu nó được cài đặt trong false, vùng chứa sẽ bắt đầu bằng (on) chút no_new_priv. Ý nghĩa của tham số này đã rõ ràng ngay từ cái tên: nó ngăn vùng chứa khởi chạy các tiến trình mới với nhiều đặc quyền hơn chính nó có.

Một tác dụng phụ của tùy chọn này được đặt thành true (mặc định) là thời gian chạy vùng chứa áp dụng cấu hình seccomp ngay từ đầu quá trình khởi động. Do đó, tất cả các lệnh gọi hệ thống cần thiết để chạy các quy trình thời gian chạy nội bộ (ví dụ: cài đặt ID người dùng/nhóm, loại bỏ một số khả năng nhất định) phải được bật trong cấu hình.

Đến một container làm những việc tầm thường echo hi, các quyền sau sẽ được yêu cầu:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "capget",
                "capset",
                "chdir",
                "close",
                "execve",
                "exit_group",
                "fstat",
                "fstatfs",
                "futex",
                "getdents64",
                "getppid",
                "lstat",
                "mprotect",
                "nanosleep",
                "newfstatat",
                "openat",
                "prctl",
                "read",
                "rt_sigaction",
                "statfs",
                "setgid",
                "setgroups",
                "setuid",
                "stat",
                "uname",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-pod-seccomp.json)

...thay vì những điều này:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "close",
                "execve",
                "exit_group",
                "futex",
                "mprotect",
                "nanosleep",
                "stat",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-container-seccomp.json)

Nhưng một lần nữa, tại sao đây lại là một vấn đề? Cá nhân tôi sẽ tránh đưa các lệnh gọi hệ thống sau vào danh sách trắng (trừ khi thực sự có nhu cầu về chúng): capset, set_tid_address, setgid, setgroups и setuid. Tuy nhiên, thách thức thực sự là bằng cách cho phép các quy trình mà bạn hoàn toàn không có quyền kiểm soát, bạn đang ràng buộc các cấu hình với việc triển khai thời gian chạy vùng chứa. Nói cách khác, một ngày nào đó bạn có thể thấy rằng sau khi cập nhật môi trường thời gian chạy vùng chứa (do bạn hoặc nhiều khả năng là do nhà cung cấp dịch vụ đám mây thực hiện), các vùng chứa đột nhiên ngừng chạy.

Mẹo số 1: Chạy container với AllowPrivilegeEscaltion=false. Điều này sẽ giảm kích thước của hồ sơ seccomp và làm cho chúng ít nhạy cảm hơn với những thay đổi trong môi trường thời gian chạy vùng chứa.

2. Đặt cấu hình seccomp ở cấp vùng chứa

Cấu hình seccomp có thể được đặt ở cấp độ nhóm:

annotations:
  seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"

...hoặc ở cấp vùng chứa:

annotations:
  container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"

Xin lưu ý rằng cú pháp trên sẽ thay đổi khi Kubernetes seccomp sẽ trở thành GA (sự kiện này được mong đợi trong bản phát hành tiếp theo của Kubernetes - 1.18 - xấp xỉ bản dịch).

Ít người biết rằng Kubernetes luôn có sâu bọkhiến hồ sơ seccomp được áp dụng cho tạm dừng vùng chứa. Môi trường thời gian chạy bù đắp một phần cho thiếu sót này, nhưng vùng chứa này không biến mất khỏi nhóm vì nó được sử dụng để định cấu hình cơ sở hạ tầng của chúng.

Vấn đề là vùng chứa này luôn bắt đầu bằng AllowPrivilegeEscalation=true, dẫn đến những vấn đề nêu ở đoạn 1, và điều này không thể thay đổi được.

Bằng cách sử dụng hồ sơ seccomp ở cấp vùng chứa, bạn tránh được cạm bẫy này và có thể tạo hồ sơ được điều chỉnh cho phù hợp với vùng chứa cụ thể. Việc này sẽ phải được thực hiện cho đến khi các nhà phát triển sửa lỗi và phiên bản mới (có thể là 1.18?) có sẵn cho mọi người.

Mẹo số 2: Đặt cấu hình seccomp ở cấp vùng chứa.

Trong ý nghĩa thực tế, quy tắc này thường đóng vai trò là câu trả lời chung cho câu hỏi: “Tại sao hồ sơ seccomp của tôi lại hoạt động với docker runnhưng không hoạt động sau khi triển khai vào cụm Kubernetes?

3. Chỉ sử dụng thời gian chạy/mặc định như là phương sách cuối cùng

Kubernetes có hai tùy chọn cho hồ sơ tích hợp: runtime/default и docker/default. Cả hai đều được triển khai bởi thời gian chạy container chứ không phải Kubernetes. Do đó, chúng có thể khác nhau tùy thuộc vào môi trường thời gian chạy được sử dụng và phiên bản của nó.

Nói cách khác, do thay đổi thời gian chạy, vùng chứa có thể có quyền truy cập vào một tập hợp các cuộc gọi hệ thống khác mà nó có thể sử dụng hoặc không thể sử dụng. Hầu hết thời gian chạy đều sử dụng Triển khai Docker. Nếu bạn muốn sử dụng hồ sơ này, hãy đảm bảo rằng nó phù hợp với bạn.

Hồ Sơ docker/default đã không được dùng nữa kể từ Kubernetes 1.11, vì vậy hãy tránh sử dụng nó.

Theo tôi, hồ sơ runtime/default hoàn toàn phù hợp với mục đích mà nó được tạo ra: bảo vệ người dùng khỏi những rủi ro liên quan đến việc thực thi lệnh docker run trên xe của họ. Tuy nhiên, khi nói đến các ứng dụng kinh doanh chạy trên cụm Kubernetes, tôi dám lập luận rằng hồ sơ như vậy quá mở và các nhà phát triển nên tập trung vào việc tạo hồ sơ cho ứng dụng (hoặc loại ứng dụng) của họ.

Mẹo số 3: Tạo hồ sơ seccomp cho các ứng dụng cụ thể. Nếu không thể, hãy tạo hồ sơ cho các loại ứng dụng, ví dụ: tạo hồ sơ nâng cao bao gồm tất cả các API web của ứng dụng Golang. Chỉ sử dụng thời gian chạy/mặc định như là phương sách cuối cùng.

Trong các bài đăng sau, tôi sẽ đề cập đến cách tạo hồ sơ seccomp lấy cảm hứng từ SecDevOps, tự động hóa chúng và kiểm tra chúng trong quy trình. Nói cách khác, bạn sẽ không có lý do gì để không nâng cấp lên cấu hình dành riêng cho ứng dụng.

4. Không giới hạn KHÔNG phải là một lựa chọn.

Của kiểm tra bảo mật Kubernetes đầu tiên hóa ra là theo mặc định seccomp bị vô hiệu hóa. Điều này có nghĩa là nếu bạn không đặt PodSecurityPolicy, điều này sẽ kích hoạt nó trong cụm, tất cả các nhóm không xác định cấu hình seccomp sẽ hoạt động trong seccomp=unconfined.

Hoạt động ở chế độ này có nghĩa là toàn bộ lớp cách nhiệt bảo vệ cụm bị mất. Cách tiếp cận này không được các chuyên gia bảo mật khuyến khích.

Mẹo số 4: Không có vùng chứa nào trong cụm sẽ chạy trong seccomp=unconfined, đặc biệt là trong môi trường sản xuất.

5. "Chế độ kiểm tra"

Điểm này không phải chỉ có ở Kubernetes, nhưng vẫn thuộc danh mục “những điều cần biết trước khi bắt đầu”.

Khi điều đó xảy ra, việc tạo hồ sơ seccomp luôn là một thách thức và phụ thuộc rất nhiều vào việc thử và sai. Thực tế là người dùng đơn giản là không có cơ hội thử nghiệm chúng trong môi trường sản xuất mà không có nguy cơ “đánh rơi” ứng dụng.

Sau khi phát hành nhân Linux 4.14, có thể chạy các phần của cấu hình ở chế độ kiểm tra, ghi lại thông tin về tất cả các lệnh gọi hệ thống trong nhật ký hệ thống nhưng không chặn chúng. Bạn có thể kích hoạt chế độ này bằng tham số SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp sẽ không ảnh hưởng đến luồng thực hiện lệnh gọi hệ thống nếu nó không khớp với bất kỳ quy tắc nào trong bộ lọc, nhưng thông tin về lệnh gọi hệ thống sẽ được ghi lại.

Đây là một chiến lược điển hình để sử dụng tính năng này:

  1. Cho phép các cuộc gọi hệ thống cần thiết.
  2. Chặn cuộc gọi từ hệ thống mà bạn biết sẽ không hữu ích.
  3. Ghi lại thông tin về tất cả các cuộc gọi khác vào nhật ký.

Một ví dụ đơn giản trông như thế này:

{
    "defaultAction": "SCMP_ACT_LOG",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "names": [
                "add_key",
                "keyctl",
                "ptrace"
            ],
            "action": "SCMP_ACT_ERRNO"
        }
    ]
}

(vừa-hỗn hợp-seccomp.json)

Nhưng hãy nhớ rằng bạn cần chặn tất cả các cuộc gọi mà bạn biết sẽ không được sử dụng và điều đó có khả năng gây hại cho cụm. Cơ sở tốt để lập danh sách là cơ sở chính thức Tài liệu về Docker. Nó giải thích chi tiết cuộc gọi hệ thống nào bị chặn trong cấu hình mặc định và tại sao.

Tuy nhiên, có một nhược điểm. Mặc dù SCMT_ACT_LOG được hỗ trợ bởi nhân Linux kể từ cuối năm 2017, nó chỉ mới gia nhập hệ sinh thái Kubernetes gần đây. Do đó, để sử dụng phương pháp này, bạn sẽ cần có kernel Linux 4.14 và phiên bản runC không thấp hơn v1.0.0-rc9.

Mẹo số 5: Có thể tạo hồ sơ chế độ kiểm tra để thử nghiệm trong sản xuất bằng cách kết hợp danh sách đen và trắng và tất cả các ngoại lệ đều có thể được ghi lại.

6. Sử dụng danh sách trắng

Việc lập danh sách trắng đòi hỏi nỗ lực nhiều hơn vì bạn phải xác định mọi lệnh gọi mà ứng dụng có thể cần, nhưng phương pháp này cải thiện đáng kể tính bảo mật:

Chúng tôi khuyên bạn nên sử dụng phương pháp danh sách trắng vì nó đơn giản và đáng tin cậy hơn. Danh sách đen sẽ cần được cập nhật bất cứ khi nào một lệnh gọi hệ thống nguy hiểm tiềm tàng (hoặc cờ/tùy chọn nguy hiểm nếu nó nằm trong danh sách đen) được thêm vào. Ngoài ra, thường có thể thay đổi cách biểu diễn của một tham số mà không thay đổi bản chất của nó và do đó bỏ qua các hạn chế của danh sách đen.

Đối với các ứng dụng Go, tôi đã phát triển một công cụ đặc biệt đi kèm với ứng dụng và thu thập tất cả các lệnh gọi được thực hiện trong quá trình thực thi. Ví dụ: đối với ứng dụng sau:

package main

import "fmt"

func main() {
	fmt.Println("test")
}

... hãy khởi động gosystract như sau:

go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf ""%s",n" .Name}}{{- end}}' application-path

... và chúng tôi nhận được kết quả sau:

"sched_yield",
"futex",
"write",
"mmap",
"exit_group",
"madvise",
"rt_sigprocmask",
"getpid",
"gettid",
"tgkill",
"rt_sigaction",
"read",
"getpgrp",
"arch_prctl",

Hiện tại, đây chỉ là một ví dụ—sẽ có thêm thông tin chi tiết về các công cụ này.

Mẹo số 6: Chỉ cho phép những cuộc gọi bạn thực sự cần và chặn tất cả những cuộc gọi khác.

7. Đặt nền móng phù hợp (hoặc chuẩn bị cho những hành vi bất ngờ)

Hạt nhân sẽ thực thi hồ sơ bất kể bạn viết gì trong đó. Ngay cả khi nó không chính xác như những gì bạn muốn. Ví dụ: nếu bạn chặn quyền truy cập vào các cuộc gọi như exit hoặc exit_group, container sẽ không thể tắt chính xác và thậm chí một lệnh đơn giản như echo hi treo anh ta lêno vô thời hạn. Kết quả là bạn sẽ nhận được mức sử dụng CPU cao trong cụm:

Seccomp trong Kubernetes: 7 điều bạn cần biết ngay từ đầu

Trong những trường hợp như vậy, tiện ích có thể giải cứu strace - nó sẽ hiển thị vấn đề có thể là gì:

Seccomp trong Kubernetes: 7 điều bạn cần biết ngay từ đầu
sudo strace -c -p 9331

Đảm bảo rằng cấu hình chứa tất cả lệnh gọi hệ thống mà ứng dụng cần trong thời gian chạy.

Mẹo số 7: Hãy chú ý đến chi tiết và đảm bảo tất cả lệnh gọi hệ thống cần thiết đều được đưa vào danh sách trắng.

Đến đây là phần kết thúc phần đầu tiên của loạt bài viết về cách sử dụng seccomp trong Kubernetes theo tinh thần SecDevOps. Trong các phần sau, chúng ta sẽ nói về lý do tại sao điều này lại quan trọng và cách tự động hóa quy trình.

Tái bút từ người dịch

Đọc thêm trên blog của chúng tôi:

Nguồn: www.habr.com

Thêm một lời nhận xét