機能を有効にする
K8s 1.18でAlphaの機能として提供されたServiceAccountIssuerDiscoveryは、K8s APIサーバでOIDC DiscoveryやJWKsのエンドポイントを提供する機能です。
有効にするためにはFeatureGateで ServiceAccountIssuerDiscovery=true
を設定する必要があります。そのうえで kube-apisever の --service-account-issuer
を指定する必要があります。このとき、 service-account-issuer
の値はエンドポイントのURLとして使われる(${service-account-issuer}/.well-known/openid-configuration
)ため、scheme=httpsを含めた形でissueを指定しておく必要があります。
e.g.,
kube-apiserver \
--feature-gates=ServiceAccountIssuerDiscovery=true # <= 該当のFeatureGateを有効にする
--service-account-issuer=https://kubernetes.default.svc # <= issueを https:// 付きで指定
--service-account-signing-key-file= ...
--service-account-api-audiences=api
--service-account-key-file= ...
...
ServiceAccountIssuerDiscovery
で提供されるエンドポイントはRBACによって制限されています。機能が有効になると system:service-account-issuer-discovery
というClusterRoleが作られる(中身はconfigurationとjwksエンドポイントに対するgetの権限付与)ので、それを運用しているクラスタのセキュリティレベルに合わせてバインディングします。
$ kubectl get clusterrole system:service-account-issuer-discovery
NAME CREATED AT
system:service-account-issuer-discovery 2020-04-10T00:42:18Z
認証済みのユーザに全てにアクセスを許可する場合には以下のようなClusterRoleBindingを作成します。
$ cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-discovery
subjects:
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: system:service-account-issuer-discovery
apiGroup: rbac.authorization.k8s.io
EOF
権限まわりの準備ができたところで、適当にデバッグ用のPodを作って動作を確認してみます。
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: debug
namespace: default
---
apiVersion: v1
kind: Pod
metadata:
name: curl
spec:
containers:
- name: main
image: tutum/curl
command: ["sleep", "9999999"]
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: debug-token
serviceAccountName: debug
volumes:
- name: debug-token
projected:
sources:
- serviceAccountToken:
path: debug-token
expirationSeconds: 7200
audience: api
$ kubectl exec -it curl sh
# export TOKEN=`cat /var/run/secrets/tokens/debug-token`
# curl -k -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc/.well-known/openid-configuration
{
"issuer": "https://kubernetes.default.svc",
"jwks_uri": "https://172.17.0.3:6443/openid/v1/jwks",
"response_types_supported": ["id_token"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"]
}
# curl -k -H "Authorization: Bearer $TOKEN" https://172.17.0.3:6443/openid/v1/jwks
{
"keys": [{
"use": "sig",
"kty": "RSA",
"kid": "_FYGic7zVHrx.....8sXADzrDCFHZhr0eA",
"alg": "RS256",
"n": "vfA4JnnOHd1pMR.....Br6I6a_mxlzw5LFbi0g9zPIPLuwm7awe_jg_wbJbMiJDzZU_ln-Ge3HusCTQ",
"e": "AQAB"
}]
}
想定どおり、K8s APIサーバが提供するDiscoveryエンドポイントからOpenID Configurationの情報や、JWKsエンドポイントからSA Tokenの署名を検証するための鍵情報を取得できました。
JWKsエンドポイントで提供されている鍵情報を使ってSA Tokenを検証してみる w/ OPA
こここからは応用編として、OpenID Connect Discoveryで取得できる情報を使って、OPAにSA Tokenを検証させてみたいと思います。ServiceAccountIssuerDiscoveryを有効にしたクラスタにOPA(とkube-mgmt)をデプロイします。
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: opa
name: opa
spec:
replicas: 1
selector:
matchLabels:
app: opa
template:
metadata:
labels:
app: opa
spec:
containers:
- name: opa
args:
- run
- --server
- --log-level=debug
image: openpolicyagent/opa:0.18.0
imagePullPolicy: IfNotPresent
- name: mgmt
args:
- --opa-url=http://127.0.0.1:8181/v1
- --enable-policies=true
- --policies=default
- --replicate-path=kubernetes
- --replicate=v1/pods
image: openpolicyagent/kube-mgmt:0.10
imagePullPolicy: IfNotPresent
lifecycle:
postStart:
exec:
# 今回の場合はOPAからDiscoveryのエンドポイントにアクセスする際の認証に必要。もうすこしいいやりかたあると思います。
# system:unauthenticated に権限を与えるような場合にはこの処理は不要。
command:
command:
- sh
- -c
- >
apk add curl &&
echo {\"token\"":" \"$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\"} > /tmp/token.json &&
curl "localhost:8181/v1/data/credentials" -X PUT -d @/tmp/token.json
serviceAccountName: opa
volumes:
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: opa
name: opa
---
apiVersion: v1
kind: Service
metadata:
labels:
app: opa
name: opa
spec:
selector:
app: opa
ports:
- protocol: TCP
port: 8181
targetPort: 8181
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: for-kube-mgmt
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- patch
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: opa
subjects:
- kind: ServiceAccount
name: opa
namespace: default
roleRef:
kind: ClusterRole
name: for-kube-mgmt
apiGroup: rbac.authorization.k8s.io
EOF
OPAに設定するPolicyはこんな感じです。RegoにはJWTを検証する関数が組み込まれているので、それをつかって簡単にPolicyを定義できます。
$ cat <<EOF | kubectl apply -f -
kind: ConfigMap
apiVersion: v1
metadata:
labels:
openpolicyagent.org/policy: rego
name: sa-token-validation.rego
data:
system-main.rego: |
package system
import data.credentials
main = response
default response = {
"valid": false,
"status": {
"reason": "Invalid SA Token",
}
}
response = {
"valid": true
} {
is_valid
}
authz_header := {"Authorization": sprintf("Bearer %s", [credentials.token]) }
discovery_req := {
"method": "GET",
"headers": authz_header,
"tls_insecure_skip_verify": true, # 注意: 本来はCA証明書渡してセットすべき
"url": "https://kubernetes.default.svc/.well-known/openid-configuration"
}
discovery_res := http.send(discovery_req)
jwks_req := {
"method": "GET",
"headers": authz_header,
"tls_insecure_skip_verify": true, # 注意: 本来はCA証明書渡してセットすべき
"url": discovery_res.body.jwks_uri
}
jwks_res := http.send(jwks_req)
constraints = {
"iss": "https://kubernetes.default.svc",
"aud": "api",
"cert": jwks_res.raw_body
}
out := io.jwt.decode_verify(input.token, constraints)
is_valid {
out[0]
}
EOF
ConfigMapで設定したPolicyはkube-mgmtがロードしてくれるように設定しています。正しく読み込まれた場合にはConfigMapのオブジェクトにアノテーションが付与されます。
kubectl get configmap sa-token-validation.rego -o yaml
kind: ConfigMap
metadata:
annotations:
openpolicyagent.org/policy-status: '{"status":"ok"}' # <= 読み込まれるとこのようなアノテーションが付与される
labels:
openpolicyagent.org/policy: rego
....
OPAの準備ができたので、先程のデバッグ用のコンテナからOPAにリクエストしてみます。正しいSA Tokenを渡した場合にはvalid=trueの結果が返ってきました。
# export TOKEN=`cat /var/run/secrets/tokens/debug-token`
# curl -X POST -d '{"token": "'"$TOKEN"'" }' http://opa.default.svc:8181/
{"valid":true}
# curl -X POST -d '{"token": "'"$INVALID-TOKEN"'" }' http://opa.default.svc:8181/
{"status":{"reason":"Invalid SA Token"},"valid":false}
OPAがJWKの内容をキャッシュしたりする方法があるのかはわかりませんが、このままだとOPAにリクエストがくるたびにJWKsを取得しにいってしまうため、K8s APIサーバに毎回検証を依頼するよりは幾分かましなものの、実運用にはもう少し工夫がいりそうです。
このあたりのIssueの議論に期待したいですね。
https://github.com/open-policy-agent/opa/issues/2057
まとめ
これまでSA Tokenを検証するとしても、検証用の鍵を動的に取得する方法がなかったため、TokenReview APIを使わざるを得ない状況でした。しかし、大規模環境のおいてSA Tokenの検証が同時に発生してしまうような場合※にはK8s APIサーバの負荷軽減を考える必要があり、そのような場合にはアプリケーション自身やその他OPAなどの仕組みでSA Tokenを検証できることが重要になってきます。 そのような場合には今回紹介した機能がとても重要になってきます。
現時点ではalpha版のため、仕様などが変わる可能性があります。必要になった場合には適切に使えるように引き続きウォッチしていきます。
※ たとえば、SPIRE の K8s NodeのNode Attestaion(k8spsat)なんかでもSA Tokenの正しさによってノードを認証しているのですが、Tokenの検証にはTokenReview APIを利用しているため、一度に大量のノードが起動してきた場合にはAPIサーバに負荷がかかってしまいます。