Kekeの日記

エンジニア、読書なんでも

Cloud Functions, Kubernetes, DockerなどにセキュアにSecretを渡す時に使えるCloud Key Management Service

f:id:bobchan1915:20181002032329p:plain

本記事

本記事はCloud Key Management Service(KMS)を使うことによってSecretをセキュアにアプリケーションに保存するを解説します。

また、本記事ではSecretをGitHub内などで管理するための手法も解説します。

アジェンダ

本日のアジェンダは以下の通りです。

環境変数について

Cloud Function

https://www.totalsolution.biz/cms/wp-content/uploads/2016/03/gcf_email_img.png

環境変数を渡す

Cloud Functionでは以下のように環境変数を渡すことができます。

Key valueで渡す

gcloud beta functions deploy FUNCTION_NAME --set-env-vars FOO=bar FLAGS...

ファイルで渡す

gcloud beta functions deploy FUNCTION_NAME --env-vars-file .env.yaml FLAGS...

環境変数を更新する

gcloud beta functions deploy FUNCTION_NAME --update-env-vars FOO=bar

環境変数を取得する

gcloud functions call envvars

Kubernetes(GKE)

https://www.publickey1.jp/2017/gke01.gif

1. 環境変数を渡す

1.1 コンテナにenvフィールドで渡す

以下のようにKey-Valueでspec.containers.envで渡せます。

spec:
    containers:
    - name: hoge
       image: hoge:latest
       env:
       - name: DB_NAME
          value: hoge

2. Secretを渡す

まず、Secretの使い方としては以下の通りです。

以下のようにマニフェストファイルを書きます。 マニフェストファイルから生成する場合はスキーマレスで、利便性の高いtype: Opaqueにします。

.dataに列挙していきます。

apiVersion: v1
kind: Secret
metadata:
    name: hoge-secret
type: Opaque
data: 
    username: cm3cero=
    password: 99hah3gpreuoa
2.1 envでコンテナに渡すときsecretKeyRefで参照
  • valueFrom.secretKeyRef.nameどのSecretかを指定します
  • valueFrom.secretKeyRef.keyそのSecretのどのKeyのValueを取るのかを指定します。
spec:
    containers:
    - name: hoge
       image: hoge:latest
       env:
       - name: DB_NAME
          valueFrom:
              secretKeyRef:
                  name: hoge-secret
                  key: username
2.2 envFromでコンテナに渡すときsecretRefでまとめて参照

これはKey一つ一つ定義しないといけないので、ファイル全体から構築する方法があります。secretKeyRefからsecretRefに変えることでできます。

「一つずつはenvを使って、ファイルとしてマウントするものはenvFromを使う」と覚えるといいです。

spec:
    containers:
    - name: hoge
       image: hoge:latest
       envFrom:
       - secretRef:
              name: hoge-secret

また、複数のファイルから開くこともあるのでenvFrom[].prefixでプレフィックスをつけることができます。

2.3 Volumnとしてマウントをする

少し重複していますがファイルなどでシークレットを管理している場合、マウントして使うこともできます。

以下の様にマウントすることもできます。

volumnMounts.mountPathで**どこにマウントするかを指定します。

spec:
    containers:
    - name: secret-container
       image: nginx:latest
       volumnMounts:
       - name: config-mount
          mountPath: /config
    volumns:
    - name: config-volumn
       secret:
            secretName: hoge-secret
            items:
            - key: username
              path: username.txt

ここでsecretNameでどのSecretを使うかを指定しています。ここではitemsとして指定しています。

また、全体としてマウントするならば

spec:
    containers:
    - name: secret-container
       image: nginx:latest
       volumnMounts:
       - name: config-mount
          mountPath: /config
    volumns:
    - name: config-volumn
       secret:
            secretName: hoge-secret

で渡すことができます。もちろん/config以下にあるだけなのでアプリケーションから呼び出さないといけません。

2.4 ファイルを渡す

環境変数を.envとしてファイルに設定する機会はあると思います。そのようなときに、どのように渡すかを設定します。

ファイルとして渡したい場合はstringDataを使うとcreateapplyのときにエンコードをしてくれます。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
stringData:
  config.yaml: |-
    apiUrl: "https://my.api.com/api/v1"
    username: user
    password: hogehoge

もちろん自分でconfig.yamlをエンコードして.dataにしてもよいですが**同じフィールドがdatastringDataで定義されている場合はstringDataが優先されるので注意が必要です。等価のコマンドは以下のようになります。

kubectl create secret generic mysecret --from-file=config.yaml=config.yaml 

また、以下のように直接、対象ファイルを貼り付けてもいいです。

apiVersion: v1
kind: Secret
metadata:
  name: devdojo-tokucha-association-api
data:
  gcloud_service_key: <base64string>

このときは、

         volumeMounts:
            - name: secret-dir
              mountPath: "/etc/secr
volumes:
 - name: secret-dir
   secret:
   secretName: devdojo-tokucha-association-api
   items:
   - key: gcloud_service_key
      path: gcloud-service-key.json

のようにマウントします。

ConfigMapなら、以下のようにします。

kubectl create configMap myConfig --from-file=configure-pod-container/configmap/

以下のように設定をします。マウントの方法としては他と同じです。

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    - name: foo
      mountPath: /etc/foo
      readOnly: true
  volumes:
  - name: foo
    secret:
      secretName: mysecret
      items:
      - key: username
        path: my-group/my-username

例えば.env/以下に置きたいときは以下のようにしないといけません。

mountPath: /.env
subPath: .env

subPathをつけないと.env/.envのしてマウントされてしまいます。

または以下の通りでvolumes[].secret.secretNameとして指定することもできます。/etc/foo/config.ymlとしてマウントされます。

volumns:
    - name: config-volumn
       secret:
            secretName: hoge-secret

3. ConfigMapとしてマウントする

以下のようにマニフェストファイルを定義します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: hoge-config
data:
    username: Hoge
    password: keisuke
3.1 コンテナにenvFromフィールドconfigMapRefで渡す
         ...
          envFrom:
          - configMapRef:
              name: hoge-config
3.2 Volumeとしてまとめてマウントする

そしてvolumnとしてPodのマニフェストファイルに定義します。

volumes:
    - name: config-volume
      configMap:
        name: hoge-config

これを使ってマウントします。

volumeMounts:
        - name: hoge-volume
          mountPath: /etc/config

全体としては以下のようになります。

apiVersion: v1
kind: Pod
metadata:
  name: pod-practice-config
spec:
  containers:
    - name: hoge-container
      image: busybox
      command: ["sleep", "3600"]
      volumeMounts:
        - name: hoge-volume
          mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: hoge-config

Envの取得

以下のようにPodにアクセスすれば環境変数を取得することができます。

kubectl exec -it hoge-pod env ; echo 

Secretの取得

以下のようにするとSecretを取得することができます。

kubectl get secret --namespace default [Deploy名] -o jsonpath="{.data.password}" | base64 --decode ; echo

Docker

https://www.docker.com/sites/default/files/social/docker_twitter_share_new.png?4362984378

実行時に渡す

以下のようにKey-Valueで渡すことができます。

docker run hoge -e DB_PASSWORD='foo'

Build値のときに書き込む(Dockerfile)

以下の様に環境変数を書き込むことができます。

ENV <key> <value>
ENV <key>=<value> ...

環境変数を取得する

以下のコマンドで環境変数を取得できます。

docker run -it hoge env

問題

上記の「環境変数を取得」などの項目でもあるように、どれも平文で保存されているため、アクセスできれば取得ができてしまいます。

解決策

https://cloud.google.com/kms/images/resources2.png?authuser=3&hl=ja

Google Cloud Platformが提供しているCloud Key Management Serviceを使うことによって暗号化・復号化することができます。

流れとしては以下のような感じです。

Client:                plain text -> KMS encrypt -> base64 encode -> 89gpehurc=
Application:       plain text <- KMS decrypt <- base64 decode <-

使い方

暗号化

以下のようにして、Keyringから作成したKeyを元に暗号化をします。

echo [実際のパスワードなど] | gcloud kms encrypt  --location=global --keyring=[KEYRING_NAME] --key=[KEY_NAME] --ciphertext-file=- --plaintext-file=- | base64

この値をenvなり、Secretで渡します。

また、同時に鍵リソースIDも教える必要があります。

復号化

以下の様にして復号します。Shellで行う場合は、以下の通りです。

echo "[先程の結果]" | base64 -D | gcloud kms decrypt --location=asia-northeast1 --keyring=mercari-kot-agent-keyring --key=mercari-kot-agent-key --ciphertext-file=- --plaintext-file=-

まず、base64 decodeをしたあとに以下の様にAPIを使います。

func main() {
        projectID := "your-project-id"
        // Location of the key rings.
        locationID := "global"

        // Authorize the client using Application Default Credentials.
        // See https://g.co/dv/identity/protocols/application-default-credentials
        ctx := context.Background()
        client, err := google.DefaultClient(ctx, cloudkms.CloudPlatformScope)
        if err != nil {
                log.Fatal(err)
        }

        // Create the KMS client.
        kmsService, err := cloudkms.New(client)
        if err != nil {
                log.Fatal(err)
        }

        // The resource name of the key rings.
        parentName := fmt.Sprintf("projects/%s/locations/%s", projectID, locationID)

        // Make the RPC call.
        response, err := kmsService.Projects.Locations.KeyRings.List(parentName).Do()
        if err != nil {
                log.Fatalf("Failed to list key rings: %v", err)
        }

        // Print the returned key rings.
        for _, keyRing := range response.KeyRings {
                fmt.Printf("KeyRing: %q\n", keyRing.Name)
        }
}

APIはdecryptを使うのでjsonドキュメントを参考にします。

"decrypt": {
                      "description": "Decrypts data that was protected by Encrypt. The CryptoKey.purpose\nmust be ENCRYPT_DECRYPT.",
                      "flatPath": "v1/projects/{projectsId}/locations/{locationsId}/keyRings/{keyRingsId}/cryptoKeys/{cryptoKeysId}:decrypt",
                      "httpMethod": "POST",
                      "id": "cloudkms.projects.locations.keyRings.cryptoKeys.decrypt",
                      "parameterOrder": [
                        "name"
                      ],
                      "parameters": {
                        "name": {
                          "description": "Required. The resource name of the CryptoKey to use for decryption.\nThe server will choose the appropriate version.",
                          "location": "path",
                          "pattern": "^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$",
                          "required": true,
                          "type": "string"
                        }
                      },
                      "path": "v1/{+name}:decrypt",
                      "request": {
                        "$ref": "DecryptRequest"
                      },
                      "response": {
                        "$ref": "DecryptResponse"
                      },
                      "scopes": [
                        "https://www.googleapis.com/auth/cloud-platform"
                      ]
                    },

GitHubなどでSecretを管理

再度掲載しますが、以下のような流れで保存しています。

Client:            plain text ->  KMS encrypt -> base64 encode 
Application:       plain text <- KMS decrypt   <- base64 decode <-

これはGithubに保存するときも同様のことができて、89gpehurc=をアップして、あとは各開発者がKeyとKeyringを使って各デベロッパーが復号化して手元で使うこともできます。

どんなSecretでも使うことができるので便利です。

(付録) Base64の役割

実際は暗号方式のための前処理だと考えることができる。

詳しく知りたい人は以下のリンクを参照してください。

Base64 - Wikipedia

まとめ

  • 環境変数はSecretや機密性の高い情報を扱うために設定するものではない
  • アプリケーション側でCloud Key Management Serviceを使うことによりセキュアに管理できる
  • この手法はGitHubで管理したり、SlackでSecretを投げる際も使える方法である