PipeCDを使ってSecretsをGitで極力安全に管理する

Gitを活用して宣言的にアプリケーションの構成管理をし、それぞれの環境へアプリケーションをデリバリを行うGitOpsスタイルは一定の支持を得て定着しつつあるスタイルになったと言ってよい。それでもGitOpsを実践する上で、議論を避けて通れない点が1つある。それはSecrets(機密情報)の管理にほかならない。

多くの開発者にSecrets管理の重要性は理解されていても、実際に継続的な管理・運用となると Secretsの管理は難しい。今でこそエンジニアの基本的な嗜みとして、各企業での基礎教育やオンボーディングも徐々に成熟しつつあるかもしれないが、それでもシークレットの外部への流出による個人情報の流出や、クラウド破産といった事故は絶えない。

GitOpsで管理するSecrets

宣言的なGitOpsスタイルにおいて、開発者はGitリポジトリを唯一の情報源(Single Source of Truth)として使用することが望ましいが、Secretを平文で配置したり、誰にでも復号できるような状態で配置することはしてはいけない。GitHubのプライベートリポジトリであっても、外界から隔離されたイントラネット内のリポジトリであろうとも、リモートワーク時代において個人PCからの流出リスクは無視できない。流出しても、第三者がどう頑張ってもシークレットを復号できない状態にすることが重要となる。

例えばKubernetesアプリケーションなら、安全にSecretを管理する代表的な例としては次のような方法が挙げられる。

1. bitnami-labs/sealed-secrets

Bitnamiが開発しているSealed Secretsは非常に有名だ。これはKubernetesクラスタ内にデプロイするsealed-secrets-controllerによって暗号化と復号が行われ、クラスタ内にKubernetesのSecretリソースが生成され、アプリケーションからはそのSecretリソースを参照する。Secretを直接Gitで宣言するのではなく、SealedSecretリソースを宣言する。鍵を流出させない限り、第三者がSealedSecretリソースから復号することは難しい。

運用面では完全にセルフマネージとなるので、秘密鍵のバックアップやリカバリ、キーローテートといった考慮が欠かせない。

2. External Secrets

External SecretsはSecretsの暗号化及び永続化を外部のSecret Manager(AWSやGCPの)に任せてしまい、アプリケーションの宣言ではそのSecrets Managerを参照し、実行時にフェッチして復号する手法である。

Kubernetes External Secretsをクラスタに構築することで、外部のSecret Managerと連携できる。External Secretsはこれを利用して、クラスタにSecretリソースを生成する。代行するExternal Secretsを宣言する。

BitnamiのSealed Secretsとは違ってパブリッククラウドのSecrets Managerを利用すれば、鍵の管理やローテートが完全にフルマネージされ、継続的な運用という観点で利点がある。

以上、2つの代表的な手法を紹介したが、本エントリではもう一つの手法として継続的デリバリーシステムである「PipeCD」を利用してSecretを管理する例を紹介する。

PipeCD

弊社CyberAgentのDeveloper Productivity室は、昨年秋に継続的デリバリソフトウェアである「PipeCD」をOSSとして公開した。PipeCDはKubernetesアプリケーションはもちろん、Google Cloud RunAWS Lambdaといたサーバレスアプリケーション、TerraformでのIaCの実現等、昨今のメインストリームであるランタイムやインフラのデリバリをGitOpsスタイルで実現する。

まず、PipeCDのアーキテクチャを簡単に紹介しておこう。

図の通り、左側のControl Planeと右側のpipedという常駐アプリケーションによってPipeCDは成立している。最近だとGitHub Actionsに、従来のように中央集権的なCIのRunnerを配置するのではなく、Runnerを開発者自身が管理する環境に配置するself-hosted runnerというスタイルがあるが、PipeCDも同様のself-hosted runnerモデルを採用している。このスタイルの良い点は自身でrunnerを構築・カスタマイズできるのと、成果物やSecretsや外部に渡す必要がないということにある。

PipeCDのSealed Secrets機能

PipeCDはGitOpsの実現をサポートするための継続的デリバリツールのため、前述のBitnamiのSealed SecretsやExternal Secretsを組み合わせてSecertsを管理することが可能だ。ArgoCDFluxCDといったツールも同様である。

これらの手法は強力ではあるが、GitOpsの実現とSecretsの管理は切っても切れない関係にある中で、セットアップや継続的な運用という観点では少し課題がある。そこで、PipeCDではもう少し簡単にSecrets管理ができるように、独自のSealed Secrets機能を実装した。

PipeCDのSealed Secrets機能では、公開鍵暗号方式によりSecretsを暗号化及び復号を行う。

暗号化のキーペア(後述)については、pipedのクラスタへのインストール・アップデート時に設定でき, pipedがAPIを経由し、自動で公開鍵をControl Planeのデータストアに永続化する。

ユーザーはWeb UIから暗号化のAPIを呼び出し、生のSecretsを送信する。データストアに公開鍵が登録されているので、APIはこれを用いて暗号化を行い、その結果をレスポンスとして返す。もちろん、生のSecretsがControl Planeで永続化されることはない。気になる人は実装1を見てほしい。

暗号化RSAキーペアの設定

PipeCDのSealed Secrets機能を利用するために、RSAキーペアを作成し設定する。作成した秘密鍵については、TeamPasswordやBitwarden等のチームで使えるパスワードマネージャで厳密に管理しておくと良いだろう。pipedはKubernetes環境であればhelmを用いてデプロイできるので、このときにRSAキーペアのパスをそれぞれ設定する。

$ helm upgrade -i piped pipecd/piped --version=v0.9.5 \
  --set-file config.data=/path-to-path/your-piped-config.yaml \
  --set-file secret.pipedKey.data=/path-to-path/your-piped-key-file \
  --set-file secret.sshKey.data=/path-to-path/your_github_id_rsa_file \
  --set-file secret.sealedSecretSealingKey.publicKey.data=/path-to-path/public-key-file \
  --set-file secret.sealedSecretSealingKey.privateKey.data=/path-to-path/private-key-file

config.dataに指定するpipedの設定ファイルには次のようにsealedSecretManagementを設定する。

apiVersion: pipecd.dev/v1beta1
kind: Piped
spec:
  projectID: your-project-id 
  pipedID: your-piped-id
  pipedKeyFile: /etc/piped-secret/piped-key
  apiAddress: your-pipecd-domain:443
  webAddress: https://your-pipecd-domain
  syncInterval: 1m
  sealedSecretManagement:
    type: SEALING_KEY
    config:
      privateKeyFile: /etc/piped-secret/sealed-secret-sealingkey-private-key
      publicKeyFile: /etc/piped-secret/sealed-secret-sealingkey-public-key

現在は独自に作成したRSAキーペアを使ったtype: SEALING_KEYのみをサポートしているが、今後GCPやAWSのKMSをサポートする予定がある。KMSをマネージドに担わせられるようになれば、External Secretsのように運用面での利点も増すだろう。

Web UIで暗号化

Web UIのApplication一覧からSecretsの暗号化ができる。

ENCRYPTをクリックすると、暗号化結果が返される。Use base64 encoding before encrypting the secretにチェックを入れると、事前にbase64エンコードをかけることができる。

この結果を利用し、PipeCDのSealed Secretsのマニフェストとして定義し、リポジトリに配置することになる。

マニフェストの作成

試しに任意のディレクトリを作成し、そこにtest-sealed-secret.yamlというマニフェストを作成しよう。spec.encryptedItemsの下に任意の属性として、先程Web UIで生成された暗号化結果文字列をそのまま記述する。

apiVersion: pipecd.dev/v1beta1
kind: SealedSecret
spec:
  template: |
    apiVersion: v1
    kind: Secret
    metadata:
      name: test-secret 
    data:
      value: {{ .encryptedItems.value }}
  encryptedItems:
    value: AQB9Ohiz+mpWf8gIvaDdnVIEH01eId3MGkwcKY7MumXW4dQS0VFNza3sHkLNgmYic4WTrkkSElFLZL7/VSn6oLUyN3z3HH6bbM3Z2kVO9HOvplhIC/E8XvXP8y5rn0dNWeIsKIm8z/E7IaKBgjgSOMZNCBsuQDvtoN3zx9QGIxLHpCZd0wWQGfvT1EVARJUUtWR4f/GXAJvCej0Se1saWAK8h6glcBBQcT4fXuPBS+ryAixGh4LWs88S9HiUCzm1Em7O5UxlkWkqlkf3MTcZNfNvBQSQIv9ulCEySfeOO9Vz56qJHgfZ1e0lIin1RSM9I73m5suSk3iQtO/bybG40DXFVlCFZD+T4Z631Btq4lMHwu20vKMvUgvTwnOOK8P9D1a2JpGBctV/y411ZDI=

Kubernetesクラスタ内でpipedがこの情報を元にSecretリソースを生成する。そのSecretsのテンプレートとなるのがspec.template属性だ。このテンプレートの中ではspec.encryptedItemsの値をプレースホルダとして埋め込めるようになっている。Secretリソース生成時に、復号された状態で展開される。

typeを何も指定しなければtype: Opaqueで生成されるし、もしDocker Registryにアクセスするための認証情報をSecretリソースにしたければtype: kubernetes.io/dockerconfigjsonを指定すれば良い。

.pipe.yamlの設定し、Secretをデプロイ

PipeCDでのデリバリの対象とするために、test-sealed-secret.yamlを作成したディレクトリに.pipe.yamlという設定ファイルを配置し、次のように設定する。

apiVersion: pipecd.dev/v1beta1
kind: KubernetesApp
spec:
  sealedSecrets:
  - path: test-sealed-secret.yaml

これをリポジトリにpushすると同期を開始し、pipedがSecretをKubernetesクラスタに適用される。これにより、GitOpsで安全にSecretsをデリバリできる。

まとめ

GitOpsにおけるSecretsの安全な管理は色々手法があるため、どれを選択すべきかというのは悩ましいところであるが、標準でSealed Secretsの機能が備わっているをPipeCDを継続的デリバリのツールとして選択すれば、あまりSecrets管理で悩むことは無いのではと考えている。

今回はSecrets管理の観点でPipeCDを紹介したが、今後も様々なワークロードを紹介していくつもりである。


  1. https://github.com/pipe-cd/pipe/blob/82c949857892ae0b29b005deffa5c80db23a4859/pkg/app/api/grpcapi/web_api.go#L677 ↩︎