🌟

Istioが解決するマイクロサービスの認証・認可

2024/09/09に公開

はじめに

マイクロサービスの数が増えるにつれて発生する問題の1つに認証・認可があります。外部からの通信はもちろん、マイクロサービス間の通信についても無制限に許可されているとセキュリティのリスクが高まります。

そこで、マイクロサービスに認証・認可を実装することが必要となります。

しかし、マイクロサービスの開発では複数のチームがそれぞれの実装言語を使用して開発を行うことが可能です。そのため、認証・認可の仕組みを提供するにはマイクロサービス共通の仕様をまとめて、それぞれの言語で実装する必要があります。

この方法では、マイクロサービスを構築する度に認証・認可の仕組みを実装する必要があり、仕様に変更があれば全てのマイクロサービスで修正する必要があります。

サービスメッシュを利用することで、この課題を解決できます。サービスメッシュは各マイクロサービスのプロキシとして動作し、クライアントとサーバー間の通信を保護するPEP(Policy Enforcement Point)として機能します。

本記事では、サービスメッシュの1つであるIstioを使ったマイクロサービスの認証・認可について説明します。また、JWT Tokenを使ってマイクロサービス間の通信を制御する方法をハンズオン形式で紹介します。

Istioの認証・認可

Istioの認証・認可のアーキテクチャから紹介します。

Authentication

Istioは、PeerAuthenticationとRequestAuthenticationの2つの認証方法を提供します。

これらのポリシーが適用されると、Istioは対象となるエンドポイントに対して設定を非同期で送信します。

リクエストを送信するクライアントは適用されたポリシーを満たす必要があります。

ポリシーはクラスタ全体や特定のnamespace・podに対して設定することが可能です。

https://istio.io/latest/docs/concepts/security/#authentication-architecture

PeerAuthentication

PeerAuthenticationでは、mTLSが許可または要求されるかどうかを設定します。

Port単位でmTLSを強制するかどうか指定可能になっています。

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: foo
spec:
  selector:
    matchLabels:
      app: finance
  mtls:
    mode: STRICT
  portLevelMtls:
    8080:
      mode: DISABLE

https://istio.io/latest/docs/reference/config/security/peer_authentication/

RequestAuthentication

RequestAuthenticationでは、リクエストに含まれるJWT Tokenを検証します。

ただし、JWT Tokenを含まないリクエストには適用されないため後述するAuthorizationPolicyを使ってJWT Tokenを強制できます。また、JWTは複数指定できるため異なるプロバイダーからのJWTを受け入れることが可能になっています。

JWKsを取得するクライアントはIstiodとEnvoyから選択可能です。デフォルトでは、20分の間隔でIstiodが取得します。

https://istio.io/latest/docs/reference/commands/pilot-agent/#:~:text=PILOT_JWT_ENABLE_REMOTE_JWKS

apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: httpbin
  namespace: foo
spec:
  selector:
    matchLabels:
      app: httpbin
  jwtRules:
  - issuer: "issuer-foo"
    jwksUri: https://example.com/.well-known/jwks.json

https://istio.io/latest/docs/reference/config/security/request_authentication/

RequestAuthenticationは、最終的にEnvoyのJWT Filterに変換されます。

https://github.com/istio/istio/blob/master/pilot/pkg/security/authn/policy_applier.go

Authorization

Istioの認可ポリシーは、各istio-proxyの認可エンジンによって評価されALLOW・DENYのどちらかに決定します。

認可ポリシーは、AuthorizationPolicyによって設定可能です。

ポリシーはクラスタ全体や特定のnamespace・podに対して設定することが可能です。

https://istio.io/latest/docs/concepts/security/#authorization-architecture

AuthorizationPolicyは最終的にEnvoyのHTTP Filter、Listener Filterに変換されます。

https://github.com/istio/istio/blob/master/pilot/pkg/security/authz/builder/builder.go

AuthorizationPolicy

AuthorizationPolicyでは、actionやrulesの設定が可能です。

actionには、CUSTOM、DENY、ALLOWを設定可能です。また、複数のポリシーが設定されている場合にCUSTOM、DENY、ALLOWの順番で評価されます。

rulesには、from(送信元)、to(宛先)、when(追加の条件)の3つの項目があります。

この例では、dev namespaceのリソースに対して有効なJWT Tokenを持つGETリクエストを許可します。

一部のルールを使用するには、mTLSの有効化が必須となっています。

https://istio.io/latest/docs/concepts/security/#dependency-on-mutual-tls

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
 name: httpbin
 namespace: foo
spec:
 selector:
   matchLabels:
     app: httpbin
     version: v1
 action: ALLOW
 rules:
 - from:
   - source:
       namespaces: ["dev"]
   to:
   - operation:
       methods: ["GET"]
   when:
   - key: request.auth.claims[iss]
     values: ["https://accounts.google.com"]

https://istio.io/latest/docs/reference/config/security/authorization-policy/

JWT Tokenを使った認証・認可

ここからは、実際にIstioの認証・認可を手を動かしながら学んでいきます。

事前準備

KubernetesクラスタとIstioの構築を行います。また、JWT Tokenの発行にKeycloakを使用します。

KINDでKubernetesクラスタを作成

kind create cluster --name istio-demo --config cluster.yaml

Ingress GatewayをNodePortを使って外部に公開するためextraPortMappingsを設定しています。

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
- role: worker
  image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
- role: worker
  image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
- role: worker
  image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
  extraPortMappings:
  - containerPort: 30081
    hostPort: 30081

https://kind.sigs.k8s.io/

Istioをインストール

istioctlを使ってIstioをインストールします。

https://istio.io/latest/docs/setup/install/istioctl/

curl -L https://istio.io/downloadIstio | sh -
cd istio-1.23.0
export PATH=$PWD/bin:$PATH
istioctl install

Ingress Gatewayを公開

istio-ingressgatewayを修正します。

kubectl -n istio-system edit svc istio-ingressgateway
apiVersion: v1
kind: Service
metadata:
  name: istio-ingressgateway
  namespace: istio-system
  ports:
  - name: status-port
    nodePort: 31042
    port: 15021
    protocol: TCP
    targetPort: 15021
  - name: http2
    nodePort: 30080 <- kindでworker nodeに指定したPortに変更
    port: 80
    protocol: TCP
    targetPort: 8080
  - name: https
    nodePort: 30930
    port: 443
    protocol: TCP
    targetPort: 8443
  type: NodePort <- LoadBalancerから変更

Keycloakをインストール

公式ドキュメントに従ってインストールします。

https://www.keycloak.org/getting-started/getting-started-kube

kubectl create -f https://raw.githubusercontent.com/keycloak/keycloak-quickstarts/latest/kubernetes/keycloak.yaml

Istioのルーティング設定

Keycloakの管理画面にアクセスできるようにGatewayとVirtualServiceをデプロイします。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: public-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: public-virtualservice
spec:
  hosts:
  - "*"
  gateways:
  - public-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: keycloak
        port:
          number: 8080

Keycloakの設定

Keycloakの管理画面にアクセスしてrealmとclientを作成します。

http://localhost:30081

手順は公式ドキュメントに従います。

https://www.keycloak.org/getting-started/getting-started-kube

RequestAuthenticationとAuthorizationPolicyをデプロイする

ここからは、実際にRequestAuthenticationとAuthorizationPolicyを使って認証・認可を実装します。

RequestAuthentication

RequestAuthenticationには、Keycloakの公開鍵のパスを設定します。この値は、http://localhost:30081/realms/myrealm/.well-known/openid-configurationjwks_uriから取得できます。

issuerには、事前準備で作成したmyrealmを設定します。

forwardOriginalTokenは、istio-proxyのバックエンドサービスにJWT Tokenを送信するかどうかを設定します。

apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: service-a
  namespace: service-a
spec:
  selector:
    matchLabels:
      app: service-a
  jwtRules:
  - issuer: "http://localhost:30081/realms/myrealm"
    jwksUri: http://keycloak.default.svc.cluster.local:8080/realms/myrealm/protocol/openid-connect/certs
    forwardOriginalToken: true

AuthorizationPolicy

ここでは、/service-a/authへのリクエストに対してDENYアクションを設定しています。

追加条件として、issuerhttp://localhost:30081/realms/myrealmではない場合にDENYします。

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: service-a
  namespace: service-a
spec:
  selector:
    matchLabels:
      app: service-a
  action: DENY
  rules:
  - to:
    - operation:
        paths: ["/service-a/auth"]
    when:
    - key: request.auth.claims[iss]
      notValues: ["http://localhost:30081/realms/myrealm"]

アプリケーション

istio-proxyのバックエンドサービスをGoを使って構築します。このアプリケーションでは、2つのパスを提供します。

/service-a/authにリクエストする際にはJWT Tokenが必須になっています。

/service-a/no-authへのリクエストにはJWT Tokenは不要になっています。

package main

import (
	"fmt"
	"net/http"
)

func authHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "This is the auth path.")
}

func noAuthHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "This is the no-auth path.")
}

func main() {
	http.HandleFunc("/service-a/auth", authHandler)
	http.HandleFunc("/service-a/no-auth", noAuthHandler)

	fmt.Println("Starting server on :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Println("Error starting server:", err)
	}
}

今回は、上記のアプリケーションをghcrにuploadして利用します。

docker build -t ghcr.io/<username>/istio-demo:v0.0.1
docker push ghcr.io/<username>/istio-demo:v0.0.1

では、Deploymentを使ってアプリケーションをデプロイします。

apiVersion: v1
kind: Namespace
metadata:
  labels:
    istio-injection: enabled
  name: service-a
---
apiVersion: v1
kind: Service
metadata:
  name: service-a
  namespace: service-a
spec:
  selector:
    app: service-a
  ports:
  - port: 8080
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-a
  namespace: service-a
spec:
  replicas: 1
  selector:
    matchLabels:
      app: service-a
  template:
    metadata:
      labels:
        app: service-a
    spec:
      containers:
      - name: service-a
        image: ghcr.io/<username>/istio-demo:v0.0.1 # usernameを修正
        ports:
        - containerPort: 8080

Istioのルーティング設定

/service-aに到達できるようにVirtualServiceを修正します。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: public-virtualservice
spec:
  hosts:
  - "*"
  gateways:
  - public-gateway
  http:
  ### 追記
  - match:
    - uri:
        prefix: /service-a
    route:
    - destination:
        host: service-a.service-a.svc.cluster.local
        port:
          number: 8080
  ###
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: keycloak
        port:
          number: 8080

動作確認

まずは、有効なJWT TokenをKeycloakから取得します。事前準備で作成したClientを使ってTokenを発行します。

> curl http://localhost:30081/realms/myrealm/protocol/openid-connect/token -d "grant_type=password&username=myuser&password=12345&client_id=myclient&client_secret=GMf7klyDB07lf7BRlh8VUypNhihmlea0&scope=openid"

では、実際にリクエストしてみます。

> curl http://localhost:30081/service-a/no-auth
This is the no-auth path.%

> curl http://localhost:30081/service-a/auth
RBAC: access denied%

> curl -H "Authorization: Bearer xxx"  http://localhost:30081/service-a/auth
This is the auth path.%

/service-a/authはJWT Tokenが無い場合にRBAC: access denied%となることを確認できました。

今回はクライアントにcurlを利用しましたがマイクロサービス間の通信でも同様にJWT Tokenを付与することでリクエストが許可されます。

最後に

いかがだったでしょうか?

Istioを活用することでマイクロサービスの認証・認可を簡単に実装できました。

今回は紹介できませんでしたが、EnvoyFilterを使って認証・認可を外部のサービスに委譲することもできます。

ぜひ、試してみてください。

Discussion