XDPプログラムをCLIでロード/アンロードする方法

iproute2

ロード

sudo ip link set dev DEVICE  [ 
    { xdp | xdpgeneric | xdpdrv | xdpoffload } 
        { object FILE [ section NAME ] [ verbose ] | pinned FILE }
]

アンロード

sudo ip link set dev DEVICE xdpgeneric off

xdp-loader

xdp-loaderはxdp-toolsについてくるコマンドツールで、XDPプログラムの簡単な操作を行える。

github.com

ロード

sudo xdp-loader load [options] <ifname> <filenames>

例えば、loにhoge.oというXDPプログラムをxdpgenericモードでロードするには以下のようにする。

sudo xdp-loader load -m skb lo hoge.o

アンロード

sudo xdp-loader unload [options]  <ifname> 

例えば、loにロードされている全てのXDPプログラムをアンロードするには以下のようにする。

sudo xdp-loader unload --all lo 

seccomp

seccompとは

Seccompとは、Linuxカーネルが持つセキュリティ機構の一つで、Secure Computing Modeの略です。 簡単に言うと、Seccompはシステムコールの許可・不許可を設定できるようにし、危険なシステムコールを実行できなくするためのものです。
突然の「Operation not permitted」-Dockerが採用するセキュリティ機構「Seccomp」とは何か? #docker #seccomp #mirantis - クリエーションライン株式会社

2つのモード

linuxのseccompにはSECCOMP_MODE_STRICTとSECCOMP_MODE_FILTERという2つのモードがある。
前者はread, write, exit, sigreturn以外のシステムコールを禁止するシンプルなものになっており、後者はBPFを利用して開発者が任意のシステムコールをフィルタできる柔軟なものになっている。

SECCOMP_MODE_STRICT

SECCOMP_MODE_STRICTで許可されるシステムコールはread, write, exit, sigreturnのみ。
違反するとプロセスにSIGKILLが送られる。

検証

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>

int main(int argc, char *argv[]) {

int before_seccomp = open("before_seccomp.txt", O_RDWR);
close(before_seccomp);

prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

int after_seccomp = open("after_seccomp.txt", O_RDWR);
close(after_seccomp);
}

実行結果

$ clang seccomp_strict_test.c -o seccomp_strict_test
$ ./seccomp_strict_test
1. invoked open system call
1. invoked close system call
Killed

prctlでseccompが呼び出した後、openシステムコールが呼び出せずに、SIGKILLが送られている。

SECCOMP_MODE_FILTER

SECCOMP_MODE_STRICTでは許可されるシステムコールが予め決まっていたが、SECCOMP_MODE_FILTERでは任意のシステムコールを指定できる。
また、違反した際のアクションについてもいろいろと設定可能になっている。例えば、プロセスSIGKILLを送るのではなく、ログに記載するだけみたいなこともできる。

検証

#include <seccomp.h>
#include <stdio.h>
#include <sys/utsname.h>
#include <unistd.h>

int main(void) {
  scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
  if (ctx == NULL) {
    goto ERROR;
  }
  int rule_add_ret;
  rule_add_ret = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(uname), 0);
  if (rule_add_ret != 0) {
    goto ERROR;
  }
  seccomp_load(ctx);

  printf("before uname invocation\n");
  struct utsname buf;
  if (uname(&buf) != 0) {
    goto ERROR;
  };
  printf("after uname invocation\n");
  printf("sysname: %s, nodename: %s, version:%s\n", buf.sysname, buf.nodename,
         buf.version);
  seccomp_release(ctx);
  return 0;

ERROR:
  perror("failed");
  return -1;
}

※prctlやseccompシステムコールを利用するのではなく、便利なlibseccompを利用している。

実行結果

$ clang seccomp_mode_filter.c -lseccomp -o filter
$ ./filter
before uname invocation
Bad system call (core dumped)

unameの呼び出しがフィルタされているのがわかる。

手元のクラスタ(kind)でgvisorを試す

kindでgvisorを使用できるようにする。

nodeに入る。(自分のケースでは1node構成なので、control-planeになっている。)

docker exec -it kind-control kind-control-plane bash

gvisorをインストールする。

以下はInstallation - gVisorにあるインストールスクリプトを少しいじったものになっている。

  set -e
  ARCH=$(uname -m)
  URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}
  wget ${URL}/runsc ${URL}/runsc.sha512 \
    ${URL}/containerd-shim-runsc-v1 ${URL}/containerd-shim-runsc-v1.sha512
  sha512sum -c runsc.sha512 \
    -c containerd-shim-runsc-v1.sha512
  rm -f *.sha512
  chmod a+rx runsc containerd-shim-runsc-v1
  mv runsc containerd-shim-runsc-v1 /usr/local/bin

containerdでrunscを利用できるように設定変更。

@@ -7,6 +7,9 @@
   type = "snapshot"
   address = "/run/containerd-fuse-overlayfs.sock"
 
+[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
+  runtime_type = "io.containerd.runsc.v1"
+
 [plugins."io.containerd.grpc.v1.cri".containerd]
   # save disk space when using a single snapshotter
   discard_unpacked_layers = true

containedを再起動

systemctl restart containerd

RuntimeClassとテスト用podの作成

RuntimeClass

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc

test用pod

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: nginx
  name: nginx
spec:
  runtimeClassName: gvisor
  containers:
  - image: nginx
    name: nginx
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}

gvisorが正しく動いていることを確認する

gvisorはuserspace kernelが動くので、コンテナ上のカーネルバージョンとノードのカーネルバージョンに差分がでるようになる。
これを確認する。

# hostmachine
6.5.0-25-generic
# nginx container
root@nginx:/# uname -r           
4.4.0 

ちゃんと差分が出てる。

一応、カーネルのログも確認してみる。

# nginx container
root@nginx:/# dmesg         
[    0.000000] Starting gVisor...
[    0.506367] Moving files to filing cabinet...
[    0.907249] Segmenting fault lines...
[    1.396441] Constructing home...
[    1.678327] Creating cloned children...
[    1.952515] Daemonizing children...
[    2.002172] Reticulating splines...
[    2.237456] Verifying that no non-zero bytes made their way into /dev/zero...
[    2.422350] Letting the watchdogs out...
[    2.890127] Creating process schedule...
[    3.020322] Consulting tar man page...
[    3.473949] Setting up VFS...
[    3.623193] Setting up FUSE...
[    4.057643] Ready!

いい感じ。。

Kubernetesにおけるetcd内のデータの暗号化

問題点:etcdには平文でデータが格納されている。

k8sの各種マニフェストははetcdに平文で保存されている。 secretであっても平文で保存されている。

twoという文字列を隠蔽したくてone-plus-oneというsecretを作成する。

$ k create secret generic one-plus-one --from-literal=ans=two

etcdの中を見てみと、twoという文字が確認できる。

$ etcdctl get /registry/secrets/default/one-plus-one | hexdump -C | grep two
000000f0  00 12 0a 0a 03 61 6e 73  12 03 74 77 6f 1a 06 4f  |.....ans..two..O|

解決策

Kubernetesにおいてetcdと通信を行うアプリはkube-api-serverのみである。
そのため、kube-api-serverでデータの暗号化と復号化を行う。つまり、データをetcdに書き込む際には暗号化して書き込みを行い、データを読み出す場合にはetcdから読み出したデータを復号化して使う。

方法

EncryptionConfigurationを用意して、それをkube-api-serverに--encryption-provider-configオプションを使って読み込ませる。

EncryptionConfiguration

サンプル (参照元: Encrypting Confidential Data at Rest | Kubernetes)

#
#
# CAUTION: this is an example configuration.
#          Do not use this for your own cluster!
#
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
      - pandas.awesome.bears.example # a custom resource API
    providers:
      - identity: {}
      - aesgcm:
          keys:
            - name: key1
              secret: c2VjcmV0IGlzIHNlY3VyZQ==
            - name: key2
              secret: dGhpcyBpcyBwYXNzd29yZA==
      - aescbc:
          keys:
            - name: key1
              secret: c2VjcmV0IGlzIHNlY3VyZQ==
            - name: key2
              secret: dGhpcyBpcyBwYXNzd29yZA==
  - resources:
      - events
    providers:
      - identity: {}
  - resources:
      - '*.apps'
    providers:
      - aescbc:
          keys:
          - name: key2
            secret: c2VjcmV0IGlzIHNlY3VyZSwgb3IgaXMgaXQ/Cg==
  • .resources.resourcesで暗号化、復号化を行う対象を選択する
  • .resources.providersで暗号化、復号化を行うproviderを選択する

.resources.providersに指定するproviderの順序には以下のような意味があるので、注意が必要。

  • 暗号化する際: .resources.providersに指定されている最初のproviderが利用される。
  • 復号化する際: .resources.providersに指定されているproviderの中で適したものが自動で選択され使用される。

各種providerの意味は下記に記載されている。

kubernetes.io

試してみる

まずはEncryptionConfigurationを作成する

#
#
# CAUTION: this is an example configuration.
#          Do not use this for your own cluster!
#
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: key1
          secret: aG9nZWhvZ2Vob2dlaG9nZQ==
    - identity: {}

aG9nZWhvZ2Vob2dlaG9nZQ==echo -n hogehogehogehoge | base64で適当に作ったものになっている。

このファイルは/etc/kubernetes/etcd/ec.yamlに保存した。(別にファイルの場所はkube-api-serverが渡せるならばどこでもよい。)

kube-api-serverに先ほど作成したEncryptionConfigurationを読み込ませる。

以下のような感じでkube-api-serverのマニフェストを修正。

@@ -13,6 +13,7 @@
   containers:
   - command:
     - kube-apiserver
+    - --encryption-provider-config=/etc/kubernetes/etcd/ec.yaml
     - --allow-privileged=true
     - --authorization-mode=Node,RBAC
@@ -92,6 +93,9 @@
     - mountPath: /usr/share/ca-certificates
       name: usr-share-ca-certificates
       readOnly: true
+    - mountPath: /etc/kubernetes/etcd
+      name: etcd
+      readOnly: true
   hostNetwork: true
   priority: 2000001000
   priorityClassName: system-node-critical
@@ -119,4 +123,8 @@
       path: /usr/share/ca-certificates
       type: DirectoryOrCreate
     name: usr-share-ca-certificates
+  - hostPath:
+      path: /etc/kubernetes/etcd
+      type: DirectoryOrCreate
+    name: etcd
 status: {}

one-minus-oneというsecretを新たに作成してみる

k create secret generic one-minus-one --from-literal=ans=zero

etcdctlでちゃんと暗号化されているか確認

$etcdctl get /registry/secrets/default/one-minus-one | hexdump -C | grep  zero

大丈夫そう

元々あったone-plus-oneというsecretも暗号化してみる

この時点では元々あったsecretは暗号化されてない。

$ etcdctl get /registry/secrets/default/one-plus-one | hexdump -C | grep two
000000f0  00 12 0a 0a 03 61 6e 73  12 03 74 77 6f 1a 06 4f  |.....ans..two..O|

元々あったsecretを全て再作成する。

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

改めてetcdctlでone-plus-oneを見てみる

$ etcdctl get /registry/secrets/default/one-plus-one | hexdump -C | grep two

何も出ない。つまり、暗号化された。

参照

kubernetes.io

etcdv3の探索でetcdctl lsはできない

etcdv3ではもはやetcdctl lsはできない。。。
というより、もはやディレクトリライクな構造になっていないようだ。

似たようなことをやりたければ以下のようにする。

etcdctl get / --keys-only --prefix

NodeRestriction

NodeRestrictionはKuberntesによって提供されるAdmissionPluginでkube-api-serverで--enable-admission-plugins=NodeRestionを指定することで有効化できる。

kubernetes.io

効果1

nodeが他のnodeのラベルを変更することを禁じる。

効果2

nodeにおけるnode-restriction.kubernetes.io/プレフィックスがついたラベルの変更を禁じる。(自分自身のnodeであっても禁止)

検証

効果1を検証
root@kind-worker:/# kubectl  --kubeconfig=/etc/kubernetes/kubelet.conf label nodes kind-worker  hoge=fuga
node/kind-worker labeled
root@kind-worker:/# kubectl  --kubeconfig=/etc/kubernetes/kubelet.conf label nodes kind-worker2  hoge=fuga
Error from server (Forbidden): nodes "kind-worker2" is forbidden: node "kind-worker" is not allowed to modify node "kind-worker2"

kind-workerは自身のnodeのラベルの変更はできるが、kind-worker2のラベルを変更できない。

効果2を検証
root@kind-worker:/# kubectl  --kubeconfig=/etc/kubernetes/kubelet.conf label nodes kind-worker node-restriction.kubernetes.io/hoge=fuga
Error from server (Forbidden): nodes "kind-worker" is forbidden: is not allowed to modify labels: node-restriction.kubernetes.io/hoge

kind-workerは自身のnodeのラベルの変更であっても、node-restriction.kubernetes.io/プレフィックスがついたラベルの変更はできない。

curlでKubernetesAPI

方法

  1. caとclient-certとclient-keyをkubeconfigから抽出する。
k config view --raw  | yq .clusters[0].cluster.certificate-authority-data | tr -d '\n' | base64 -d > ca                                                           
k config view --raw  | yq .users[0].user.client-certificate-data | tr -d '\n' | base64 -d > cert                                                                 
k config view --raw  | yq .users[0].user.client-key-data | tr -d '\n' | base64 -d  > key
  1. kube-api-serverのエンドポイントとcacert, cert, keyをそれぞれ指定して、curlを実行する。
curl $(k config view | yq .clusters[0].cluster.server) --cacert ca --key key --cert cert

※ 対象のclusterとuserがkubeconfigの.clusterと.userの最初のエントリになっていることを想定している。

kubectl get podscurlでやる例

curl $(k config view | yq .clusters[0].cluster.server)/api/v1/namespaces/default/pods --cacert ca --key key --cert cert