Kubernetesを学んでいたら突如現れたCapabilityって概念

# あらまし
CKAを取得すべく、UdemyのコースでKubernetesについて学んでいたら、こんな問題が出てきました。（実際の問題からは改変しています）

> Create a new pod.　Allow the pod to be able to set system_time.

Podを作って、システムタイムの設定ができるように許可せよ。

お恥ずかしながら、Linuxはよくわかっていない私。システムタイムを設定するのに権限が必要なことも知りませんでした。
というわけで早速調べました。

# ググってみた
ひとまず問題中に出てきたキーワードで検索しました。「Pod System_time」

検索結果を眺めると「Capability」「SYS_TIME」「SecurityContext」といった単語が目立ちました。
候補の2番目にKubernetesの[公式ページ](https://kubernetes.io/ja/docs/tasks/configure-pod-container/security-context/)がありました。

クリック。

いまの私には理解できない単語が並んでいます。ブラウザバックしたい衝動に駆られながら概要だけでもと思い読んでいきました。

注目したのは次の一文。

> Linux Capabilities: rootユーザーのすべての特権ではなく、一部の特権をプロセスに与えます。

問題の中でPodに与える権限をsystem_timeの設定権限のみに限定していたことから、「Linux Capability」という単語に目星をつけました。

# またググる
「Linux Capability」でググりました。

候補には分かりやすそうな記事が並んでいましたが[UbuntuのManPageがあった](https://manpages.ubuntu.com/manpages/kinetic/ja/man7/capabilities.7.html)ので、まずは公式に当たってみます。

>権限のチェックを行う観点から見ると、伝統的な  UNIX の実装では プロセスは二つのカテゴリーに分類できる: 特権プロセス (実効ユーザーID が 0 のプロセス。ユーザーID  0  は  スーパーユーザーや  root  と呼ばれる) と 非特権 プロセス (実効ユーザーID が 0 以外のプロセス) である。
>
>非特権プロセスでは、プロセスの資格情報 (通常は、実効UID 、実効GID  と追加のグループリスト)に基づく権限チェックが行われるのに対し、特権プロセスでは全てのカーネルの権限チェックがバイパスされる。
>
 >バージョン 2.2 以降の Linux では、 これまでスーパーユーザーに結び付けられてきた権限を、 いくつかのグループに分割している。これらのグループは ケーパビリティ(capability) と呼ばれ、グループ毎に独立に有効、無効を設定できる。 ケーパビリティはスレッド単位の属性である。

- UNIXではプロセスを二種類に分けられる
- 一つ特権プロセスで、権限チェックは行われない
- もうひとつは非特権プロセスでUIDやGID、グループリストに基づく権限チェックが行われる
- バージョン2.2以降のLinuxでは権限をいくつかのグループに分けた。このグループのことをCapability（ケーパビリティ）と呼んでいる。Capabilityはスレッド単位で設定される。 


バージョン2.2以降のLinuxというのがよく分かっていません（ディストリビューションのバージョンとは違うのかな）が「権限を細かく制御できるようにバージョン2.2以降のLinuxではCapabilityという機能を導入したよ」という風に理解しました。


さて、UbuntuのManPageには続きがあり、Capabilityのリストが書かれていました。
つまりスレッドひいてはプロセスに対してリスト内のCapabilityを設定すれば、そのプロセスに対して権限を与えられそうです。

まずは閲覧してたUbuntuのManPageは日本語だったので、[英語ページ](https://manpages.ubuntu.com/manpages/kinetic/en/man7/capabilities.7.html)に移動しました。
そしてページ内検索。

「system time」　Hitしない

「system_time」　Hitしない

「time」         CAP_SYS_TIMEがヒット。


> CAP_SYS_TIME
> Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock.

これっぽいですね。
[settimeofdayはC言語の関数](https://manpages.ubuntu.com/manpages/kinetic/en/man2/settimeofday.2.html)で、システムタイムを設定するために利用するようです。
引数にtimevalとtimezoneという構造体を取りますが、timezoneは通常ではNULLにすべきとのこと。OSに設定された値を使うのでしょうか。

次に、UbuntuのManPageにはCapabilityはスレッド単位で設定すると書かれていたので、スレッドひいてはプロセスにCapabiltyを設定する方法を調べました。
Capabilityがセキュリティのコンテキストに関連する機能であることから、実行ファイルに対して後付けでCapabilityを設定するのではないかと予想します。
おそらく設定するためのコマンドがあるはずだと考えました。

「add capability linux」でググると、候補に次のタイトルがありました。
> How to set capabilities with setcap command?

# setcap
setcapがcapabilityを設定するコマンドなのかもしれません。
さっそく検索しました。
検索ワードは「setcap man」

[setcap(8) — Linux manual page](https://man7.org/linux/man-pages/man8/setcap.8.html)

> NAME
> setcap - set file capabilities

当たりです。

> SYNOPSIS 
> setcap [-q] [-n <rootuid>] [-v] {capabilities|-|-r} filename [... capabilitiesN fileN ]

オプションのcapabilitiesにUbutunのCapabilityのManPageにリストアップされていた設定を指定すれば良さそうでした。

検証に必要な情報はおおかた集められたので、検証しました。

# 検証
検証は以下の方針で行いました。
1. settimeofdayを使用してシステムタイムを変更するソフトウェアを作る
1. capabilityを設定せずソフトウェアを実行する。このとき何かしらのエラーが出るはず
1. capabilityを設定してソフトウェアを実行する。システムタイムが設定される

## 手順1
コードはGolangで実装しました。

実行環境
- Linux raspi 6.8.0-1004-raspi #4-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 20 02:29:55 UTC 2024 aarch64 aarch64 aarch64 GNU/Linux
- go version go1.22.3 linux/arm64

```go
package main

import (
	"fmt"
	"os"
	"syscall"
	"time"
)

func main(){
	if len(os.Args) < 2 {
		fmt.Println("the argument requires a date in the format 'yyyy-MM-dd hh:mm:ss'")
		os.Exit(1)
	}

	t, err := time.Parse("2006-01-02 15:04:05", os.Args[1])
	if err != nil {
		fmt.Println("failed to parse time. the argument requires a date in the format 'yyyy-MM-dd hh:mm:ss' :")
		os.Exit(1)
	}
	
	tv := &syscall.Timeval{
		Sec: t.Unix(),
		Usec: 0,
	}

	err = syscall.Settimeofday(tv)
	if err != nil{
		fmt.Println("failed to set of time :", err)
		os.Exit(1)
	}
}
```

### 使用した関数
- syscall.Settimeofday（timeval *Timeval) (err error):
    -  settimeofdayをシステムコールする関数
    -  引数のtimevalは構造体で、設定する時間の基準値Sec（UnixEpoch)と、Secから設定したい時間までの差（マイクロ秒）Usecを設定する


## 手順2
手順1で書いたコードをビルドして実行しました。
実行ファイル名はsetclockとしました。


```
./setclock "2005-06-16 00:00:00

failed to set of time : operation not permitted
```

想定通り、permission errorが発生しました。

## 手順3
次にsetcapを使ってsetclockにcapabilityを設定します。

```
setcap CAP_SYS_TIME ./setclock

fatal error: Invalid argument
```

ダメでした。setcapのUsageを確認しました。
    
    usage: setcap [-h] [-q] [-v] [-n <rootid>] (-r|-|<caps>) <filename> [ ... (-r|-|<capsN>) <filenameN> ]

     Note <filename> must be a regular (non-symlink) file.
     -r          remove capability from file
     -           read capability text from stdin
     <capsN>     cap_from_text(3) formatted file capability
     [ Note: capsh --suggest="something..." might help you pick. ]
     -h          this message and exit status 0
     -q          quietly
     -v          validate supplied capability matches file
     -n <rootid> write a user namespace (!= 0) limited capability
     --license   display the license info

usageの小かっことか初めて見た。
読み解くとひとつのファイルにCapabilityを設定する場合は「setcap <Capability> <filename>」。複数のファイルに設定する場合は「<Capability> <filename>」の部分を連ねていくっぽいですね。オプションについては今回は必要なさそうでした。

さて、エラーが出ていたのはおそらく<Capability>の部分と予想しました。
オプションの説明欄に下記のように書かれていました。
>  capsN    cap_from_text(3) formatted file capability

おそらくCAP_SYS_TIMEをcap_from_text(3)の形式するのだと思いました。

## cap_from_text(3)
「cap_from_text(3)」でググると、もはやおなじみの[ManPage](https://linux.die.net/man/3/cap_from_text)が候補にあったのでクリックしました。

>### Name
>cap_from_text, cap_to_text, cap_to_name, cap_from_name - capability state textual representation translation

Capability Stateをテキスト表現に変換するもののようですが、よくわかりませんでした。

> ### Description
> (中略)
>**cap_from_text()** allocates and initializes a capability state in working storage. It then sets the contents of this newly created capability state to the state represented by a human-readable, nul-terminated character string pointed to by buf_p. It returns a pointer to the newly created capability state. 

cap_from_text()の引数にCapabilityの種類を表す文字列を入れると、新しく作成されるcapability stateへのポインタを返すようです。
具体的なことは分かりませんが、「the state represented by a human-readable」の記法を確認しました。

> ## Textual Representation
> A textual representation of capability sets consists of one or more whitespace-separated clauses. Each clause specifies some operations on a capability set; the set starts out with all capabilities lowered, and the meaning of the string is the state of the capability set after all the clauses have been applied in order.

Capabilityの文字列表現はoperationsとセットになるようです。

>Each clause consists of a list of comma-separated capability names (or the word 'all'), followed by an action-list. An action-list consists of a sequence of operator flag pairs. Legal operators are: '=', '+', and '-'. Legal flags are: 'e', 'i', and 'p'. These flags are case-sensitive and specify the Effective, Inheritable and Permitted sets respectively.

複数のcapabilityを設定したいときはカンマで区切って最後にaction-listと呼ばれるフラグをオペレータとフラグのセットを設定するようです。このフラグは小文字大文字が区別されます。

> In the capability name lists, all names are case-insensitive. The special name 'all' specifies all capabilities; it is equivalent to a list naming every capability individually.

重要情報です。Capabilityの名前は小文字大文字が区別されないとのことでした。であれば先ほどのsetcapで指定したCAP_SYS_TIMEは間違っておらず、action-listが不足していたと推測できました。

そうすると上述のフラグとオペレーターの情報が必要ですね。

まずは同じページに記載されていたオペレーターの情報です。
>The '=' operator indicates that the listed capabilities are first reset in all three capability sets. The subsequent flags (which are optional when associated with this operator) indicate that the listed capabilities for the corresponding set are to be raised. For example: "all=p" means lower every capability in the Effective and Inheritable sets but raise all of the Permitted capabilities; or, "cap_fowner=ep" means raise the Effective and Permitted override-file-ownership capability, while lowering this Inheritable capability.

>In the case that the leading operator is '=', and no list of capabilities is provided, the action-list is assumed to refer to 'all' capabilities. For example, the following three clauses are equivalent to each other (and indicate a completely empty capability set): "all="; "="; "cap_chown,<every-other-capability>=".

>The operators, '+' and '-' both require an explicit preceding capability list and one or more explicit trailing flags. The '+' operator will raise all of the listed capabilities in the flagged capability sets. The '-' operator will lower all of the listed capabilities in the flagged capability sets. For example: "all+p" will raise all of the Permitted capabilities; "cap_fowner+p-i" will raise the override-file-ownership capability in the Permitted capability set and lower this Inheritable capability; "cap_fowner+pe-i" and "cap_fowner=+pe" are equivalent.

- = :Capabilityをリセットして、フラグに指定された種類のcapability setを設定します
- + :フラグで指定されたcapability setを設定します
- - :フラグで指定されたcapability setを削除します

各フラグについての説明はsetcapのManPageにはなかったので、探しにいきました。
見つけたのは最初に見た[CapabilityのManPage](https://manpages.ubuntu.com/manpages/kinetic/ja/man7/capabilities.7.html)
>**スレッドケーパビリティセット**
各スレッドは以下の 3種類のケーパビリティセットを持つ。各々のケーパビリティセットは  上記のケーパビリティの組み合わせである (全てのケーパビリティが無効でもよい)。
>
>**許可 (permitted):**
そのスレッドが持つことになっている実効ケーパビリティの限定的なスーパーセットである。これは、実効ケーパビリティセットに CAP_SETPCAP ケーパビリティを持っていないスレッドが継承可能ケーパビリティセットに追加可能なケーパビリティの限定的なスーパーセットでもある。
>
>許可ケーパビリティセットから削除してしまったケーパビリティは、 (set-user-ID-root プログラムか、そのケーパビリティをファイルケーパビリティで許可しているプログラムをexecve(2)しない限りは) もう一度獲得することはできない。
>
>**継承可能 (inheritable):**
execve(2)を前後で保持されるケーパビリティセットである。この仕組みを使うことで、あるプロセスがexecve(2) を行う際に新しいプログラムの許可ケーパビリティセットとして 割り当てるケーパビリティを指定することができる。
>
>**実効 (effective):**
カーネルがスレッドの権限 (permission)  をチェックするときに  使用するケーパビリティセットである。
>
>**fork(2)** で作成される子プロセスは、親のケーパビリティセットのコピーを継承する。 execve(2)中のケーパビリティの扱いについては下記を参照のこと。
>
>**capset(2)**  を使うと、プロセスは自分自身のケーパビリティセット を操作することができる (下記参照)。
> ### ファイルケーパビリティ
> (中略)
>    **許可 (Permitted) (以前の強制 (Forced)):**
>          スレッドの継承可能ケーパビリティに関わらず、そのスレッドに自動的に 認められるケーパビリティ。
>
>**継承可能 (Inheritable) (以前の 許容 (Allowed)):**
>          このセットと、スレッドの継承可能ケーパビリティセットとの  論理積  (AND)  がとられ、execve(2) の後にそのスレッドの許可ケーパビリティセットで有効となる 継承可能ケーパビリティが決定される。
>
>**実効 (effective):**
>これは集合ではなく、1ビットの情報である。このビットがセットされていると、execve(2) 実行中に、そのスレッドの新しい許可ケーパビリティが全て 実効ケーパビリティ集合においてもセットされる。 このビットがセットされていない場合、 execve(2)  後には新しい許可ケーパビリティのどれも新しい実効ケーパビリティ集合 にセットされない。

capabilityはスレッドごとに設定されて、それぞれのcapabilityは3種類のスレッドケーパビリティセットのいずれかに属するか、あるいは属していないということでした。capabilityが属しているスレッドケーパビリティセットによって動作が変わるんですね。
ファイルケーパビリティセットはsetcapによって実行ファイルに対してケーパビリティセットを設定できるようです。私がやろうとしていたことはこれでした。ファイルケーパビリティセットとスレッドケーパビリティセットを掛け合わせてスレッドに与えられる最終的なスレッドケーパビリティが決定するようです。

さて、作成したシステムタイムを設定するソフトをどのケーパビリティセットに関連付けるかですが、スレッドケーパビリティセットのeffectiveの説明にこうあります。

>実効 (effective):
>カーネルがスレッドの権限 (permission)  をチェックするときに使用するケーパビリティセットである。

おそらくeffectiveに属しているcapabilityが実際に有効なcapabilityになるので、最終的にeffectiveが設定されるようにファイルケーパビリティを設定すれば良さそうです。

深く考えず次のコマンドを実行しました。
```
 setcap CAP_SYS_TIME+pe ./setclock

 fatal error: Invalid argument
```
ダメですね。

もう一度コマンドを実行します

```
 setcap "CAP_SYS_TIME+pe" ./setclock

 unable to set CAP_SETFCAP effective capability: Operation not permitted
```

sudoをつけます
```
 sudo setcap "CAP_SYS_TIME+pe" ./setclock
```

通りました。特にメッセージが出たりしないんでしょうか。
エラーが出なかったので、設定できていると信じてソフトを実行してみます。

```
 ./setclock "2005-06-16 00:00:00"

 date
Fri May  31 21:01:38 JST 2024
```
dateの出力が"Thu Jun 16 09:00:10 JST 2005"になるはずですが、変わっていないですね。
これは時刻を同期しているsystemd-timesyncdが機能しているのだと推測しました。
なので、systemd-timesyncdを停止してからもう一度実行します。

```
systemctl stop systemd-timesyncd

 ./setclock "2005-06-16 00:00:00"

 date
 Thu Jun 16 09:00:10 JST 2005
 ```

 変更されました。

以上で検証終了です。

# まとめ
Capabilityはスレッドに権限を設定するためのセキュリティ機能。
実行ファイルのCapabilityを設定するにはsetcapを使います。
KubernetesのPodで実行されるコンテナも同じ仕組みで権限を管理できます。