この記事は、Microservices July 2023: マイクロサービスのStart Delivering Microservicesの方法を実行するための4つのチュートリアルの1つです。
- マイクロサービスのデプロイと構成方法
- コンテナ環境での安全なシークレット管理方法(この記事)
- GitHub Actionsを使ってマイクロサービスのカナリアデプロイメントを自動化する方法
- OpenTelemetryトレーシングを使ったマイクロサービスの可視化方法
お使いのマイクロサービスの多くには、安全な運用のためにシークレットが必要です。シークレットの例として、SSL/TLS証明書の秘密鍵、別のサービスで認証するAPIキー、またはリモートログイン用のSSHキーなどがあります。シークレットを適切に管理するには、シークレットを使用するコンテキストを必要な場所に制限する、およびシークレットを必要なとき以外にアクセスされるのを防止することが必要です。ただこのやり方は、アプリケーション開発が急がれる場合は、飛ばされることがよくあります。その結果はどうなるのでしょうか? シークレット管理が不適切なことで、情報漏洩や悪用される一般的な原因となります。
チュートリアルの概要
このチュートリアルでは、クライアントコンテナがサービスのアクセスに利用する JSON Web Token(JWT)を、安全に配布および利用しています。このチュートリアルには課題が4つあり、シークレット管理において4つの方法それぞれで試行し、お使いのコンテナでの正しいシークレットの管理方法だけではなく、十分とはいえない方法についても確認します。
このチュートリアルではJWTをサンプルシークレットとして使用していますが、この技法は、データベース認証情報、SSL 秘密鍵およびその他 API キー等、秘密にすることが必要なコンテナ用のものに適用されます。
チュートリアルは、2つの主要ソフトウェアコンポーネントを活用します。
- API サーバー – NGINX オープンソースといくつかの基本的な NGINX JavaScript コードを実行するコンテナで、JWT からクレームを抽出し、そのクレームの1つから値を返すか、またはクレームがない場合は、エラーメッセージを返します。
- API クライアント – 非常にシンプルなPythonコードを実行するコンテナで、
GET
リクエストをAPIサーバーに行うだけです。
チュートリアルのデモを実行するには、このビデオをご覧ください。
このチュートリアルを実行するもっとも簡単な方法は、Microservices Julyに登録し、提供されたブラウザベースのラボを使用することです。この記事には、お使いの環境でチュートリアルを実行する代替方法も記載されています。
前提条件とセットアップ
前提条件
お使いの環境でチュートリアルを完了するには、以下が必要です。
- Linux/Unix 環境
- Linux コマンドラインの基本を理解していること
nano
またはvim
等のテキストエディタ- Docker (Docker ComposeとDocker Engine Swarmを含む)
curl
(ほとんどのシステムではすでにインストール済み)git
(ほとんどのシステムではすでにインストール済み)
注:
- チュートリアルは、ポート 80 につながるテストサーバーを利用します。ポート 80 をすでに使用している場合は、
‑p
フラグを使って、docker
run
コマンドで開始するときにテストサーバーに別の値を設定します。次に、curl
コマンドで:<port_number>
サフィックスをlocalhost
に含めます。 - チュートリアル全体で、Linux コマンドラインのプロンプトは省略され、コマンドをチュートリアルにコピー&ペーストしやすくしています。チルダ (
~
) はホーム ディレクトリを表します。
セットアップ
このセクションではチュートリアル リポジトリをcloneし、認証サーバーを起動、そしてトークン付きとトークンなしで、テストリクエストを送信します。
チュートリアルリポジトリをcloneする
-
お使いのホームディレクトリで、microservices-marchディレクトリを作成し、GitHubリポジトリをそこにcloneします。(別のディレクトリ名を使用し、それにあわせて各操作を適宜変更することも可能です。) リポジトリには、構成ファイルと、別の方法でシークレットを取得する API クライアントアプリケーションの個別バージョンが含まれています。
mkdir ~/microservices-march cd ~/microservices-march git clone https://github.com/microservices-march/auth.git
-
シークレットが表示されます。これは署名されたJWTで、通常はAPI クライアントをサーバーに認証するために使用されます。
cat ~/microservices-march/auth/apiclient/token1.jwt "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
このトークンを使って認証する方法はいくつかありますが、このチュートリアルでは、API クライアントアプリケーションが、OAuth 2.0 Bearer Token認証フレームワークを使って認証サーバーに渡します。そこではこの例のように、JWTにAuthorization:
Bearer
プレフィクスを付けます。
"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
認証サーバーを構築・起動する
-
認証サーバーディレクトリに移動します。
cd apiserver
-
認証サーバー用Dockerイメージを作成します。(最後の「ピリオド(.)」に注意してください。)
docker build -t apiserver .
-
認証サーバーを起動し、正しく実行されているかを確認します。(出力は読みやすいように複数ラインで表示されます。)
docker run -d -p 80:80 apiserver docker ps CONTAINER ID IMAGE COMMAND ... 2b001f77c5cb apiserver "nginx -g 'daemon of..." ... ... CREATED STATUS ... ... 26 seconds ago Up 26 seconds ... ... PORTS ... ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ... ... NAMES ... relaxed_proskuriakova
認証サーバーをテストする
-
認証サーバーが JWT を含まないリクエストを拒否し、
401
Authorization
Required
を返しているか確認します。curl -X GET http://localhost <html> <head><title>401 Authorization Required</title></head> <body> <center><h1>401 Authorization Required</h1></center> <hr><center>nginx/1.23.3</center> </body> </html>
-
Authorization
ヘッダを使ってJWTを提供します。200
OK
の応答コードはAPIクライアントアプリケーションが正常に認証されたことを示します。curl -i -X GET -H "Authorization: Bearer `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhost HTTP/1.1 200 OK Server: nginx/1.23.2 Date: Day, DD Mon YYYY hh:mm:ss TZ Content-Type: text/html Content-Length: 64 Last-Modified: Day, DD Mon YYYY hh:mm:ss TZ Connection: keep-alive ETag: "63dc0fcd-40" X-MESSAGE: Success apiKey1 Accept-Ranges: bytes { "response": "success", "authorized": true, "value": "999" }
課題1: アプリケーションにハードコードされたシークレット (不適切な手法です)
この課題を開始する前にはっきりさせておきたいのは、アプリケーションにシークレットをハードコードすることはとんでもないことだということです!コンテナイメージにアクセスする者がハードコード化された認証情報をいかに簡単に探して抽出できるかを確認します。
この課題では、ビルドを行うディレクトリにAPIクライアントアプリケーション用のコードをコピーし、アプリケーションを構築および実行し、シークレットを抽出します。
APIクライアントのアプリケーションをコピーする
apiclientディレクトリのapp_versionsサブディレクトリには、この4つの課題のそれぞれで利用する簡単な API クライアントアプリケーションの各種バージョンが含まれ、それぞれが以前のものに比べより少し安全です (詳細はチュートリアル概要を参照してください)。
-
API クライアントディレクトリに移動します。
cd ~/microservices-march/auth/apiclient
-
この課題( ハードコード化されたシークレット)で利用するためアプリケーションを作業ディレクトリにコピーします。
cp ./app_versions/very_bad_hard_code.py ./app.py
-
アプリケーションを確認します。
cat app.py import urllib.request import urllib.error jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" authstring = "Bearer " + jwt req = urllib.request.Request("http://host.docker.internal") req.add_header("Authorization", authstring) try: with urllib.request.urlopen(req) as response: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message) except urllib.error.URLError as e: print(str(e.code) + " s " + e.msg)
このコードは、ローカルホストへのリクエストを作成し、成功メッセージまたは失敗コードを表示するだけです。
リクエストはこのラインに
Authorization
ヘッダを追加します。req.add_header("Authorization", authstring)
他に何か気づきますか?もしかしたらハードコード化された JWT があるのでは? その点についてはすぐに説明します。初めに、アプリケーションを構築して実行しましょう。
APIクライアントアプリケーションを構築および実行する
docker
compose
コマンドを、Docker Compose YAMLファイルとあわせて使用しています。これで、実際の状況が少しわかりやすくなります。
(前のセクションの手順2 で、課題 1 専用の API クライアントアプリケーションのPython ファイル名 (very_bad_hard_code.py) をapp.pyに変更したことに気づくでしょう。他の3つの課題でも同じことを行います。app.pyを毎回使用することで、Dockerfileの変更が不要なため、ロジスティクスが簡素化されます。これは、 ‑build
引数をdocker
compose
コマンドに含め、コンテナのリビルドを強制することを意味しています。)
docker
compose
コマンドは、コンテナを構築し、アプリケーションを起動し、API リクエストを1回行い、APIコールの結果をコンソールに表示しながら、コンテナをシャットダウンします。
出力の2行目から最後までの内容にある200
Success
コードは、認証に成功したことを示しています。apiKey1
の値は、認証サーバーが JWT でその名前のクレームを復号できることを示すため、詳細確認になります。
docker compose -f docker-compose.hardcode.yml up -build
...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0
このように、ハードコード化された認証情報が API ライアントアプリケーションで正常に作動しましたが、これは驚くことではありません。しかし、これは安全でしょうか? コンテナが終了するまでに一度だけこのスクリプトが実行され、シェルを持っていないのだから、安全であると言えるかもしれないでしょうか?
実際のところ、これは安全とは言えません。
コンテナイメージからシークレットを取得する
コンテナのファイルシステムを抜き出すのは簡単なことであり、認証情報をハードコードすると、コンテナイメージにアクセスできる人なら誰でも閲覧できる状態になります。
-
extractディレクトリを作成して、移動する。
mkdir extract cd extract
-
コンテナイメージについての基本情報を表示します。
--format
フラグで出力がより読みやすくなります (同じ理由で、出力が2つのラインにまたがっています。)docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTAINER ID NAMES IMAGE ... 11b73106fdf8 apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... CREATED STATUS ... 6 minutes ago Exited (0) 4 minutes ago ... 43 minutes ago Up 43 minutes
-
最新のapiclientイメージを.tarファイルとして抽出します。
<container_ID>
には、上記の出力にあるCONTAINER
ID
フィールド(このチュートリアルでは11b73106fdf8
) の値を代入します。docker export -o api.tar <container_ID>
コンテナのファイルシステム全体が含まれるapi.tarアーカイブの作成には数秒かかります。シークレットを探す方法の1つとして、アーカイブ全体を抽出して解析することができますが、面白そうなものをみつけるためのショートカットがあることが分かっています。
docker
history
コマンドでコンテナの履歴を表示することです。 (このショートカットは、Docker Hub または別のコンテナレジストリで、Dockerfileを持たずコンテナイメージのみを持つコンテナにも使えるため、特に便利です)。 -
コンテナ履歴を表示する。
docker history apiclient IMAGE CREATED ... 9396dde2aad0 8 minutes ago ... <missing> 8 minutes ago ... <missing> 28 minutes ago ... ... CREATED BY SIZE ... ... CMD ["python" "./app.py"] 622B ... ... COPY ./app.py ./app.py # buildkit 0B ... ... WORKDIR /usr/app/src 0B ... ... COMMENT ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0
出力結果の時系列が逆になっています。出力結果では、作業ディレクトリとして/usr/app/srcが設定され、その後アプリケーションの Pythonコードのファイルがコピーおよび実行されたことを示しています。このコンテナのコアコードベースが/usr/app/src/app.pyにあり、それが認証情報の場所である可能性が高いことを推察するのは、優れた探偵でなくてもできることです。
-
その知識により、そのファイルのみを抽出します。
tar --extract --file=api.tar usr/app/src/app.py
-
このようにファイルの内容を表示し、まさに“安全である” JWTへのアクセスを実施しました。
cat usr/app/src/app.py ... jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" ...
課題2: シークレットを環境変数として渡す (こちらも不適切です)
Microservices July 2023のUnit1 ”Twelve-Factor App” (アプリケーション構成ガイドライン)によるマイクロサービス・アーキテクチャの適用を完了した方は、環境変数を使って設定データをコンテナに渡すことを理解していると思います。もし見逃したとしてもご心配なく登録後にオンデマンドで利用できます。
この課題では、シークレットを環境変数として渡します。課題1で確認した方法のように、この方法も推奨されていません! シークレットのハードコード化ほど悪いものではありませんが、ご覧のとおり、弱点がいくつかあります。
環境変数をコンテナに渡すには、方法は4つあります。
-
Dockerfileで
ENV
の記述を使い、変数の置換を行う (すべての構築イメージに変数を設定)。例:ENV PORT $PORT
-
‑e
フラグをdocker
run
コマンド上で使用する。例:docker run -e PASSWORD=123 mycontainer
environment
キーをDocker Compose YAMLファイルで使用する。- 変数を含む.envファイルを使用する。
この課題では、環境変数を使ってJWTを設定し、JWTが公開されているかどうかコンテナを確認します。
環境変数を渡す
-
APIクライアントディレクトリに戻ります。
cd ~/microservices-march/auth/apiclient
-
この課題のアプリケーション(環境変数を使用するもの)を作業ディレクトリにコピーして、課題1のapp.pyファイルを上書きします。
cp ./app_versions/medium_environment_variables.py ./app.py
-
アプリケーションをご覧ください。出力に関連する行で、シークレット (JWT) は、ローカルコンテナの環境変数として読み取られます。
cat app.py ... jwt = "" if "JWT" in os.environ: jwt = "Bearer " + os.environ.get("JWT") ...
-
上記のとおり、環境変数をコンテナに入れる方法が選択できます。一貫性を保つため、Docker Composeを使用しています。Docker Compose YAMLファイルのコンテンツを表示します。このファイルは、
environment
キーを使ってJWT
環境変数を設定します。cat docker-compose.env.yml --- version: "3.9" services: apiclient: build: . image: apiclient extra_hosts: - "host.docker.internal:host-gateway" environment: - JWT
-
環境変数を設定しないでアプリケーションを実行します。APIクライアントアプリケーションがJWTを渡さなかったために認証に失敗したことが、出力の2行目から最後の
401
Unauthorized
コードから確認できます。docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 401 Unauthorized apiclient-apiclient-1 exited with code 0
-
分かりやすくするため、環境変数をローカルで設定します。セキュリティ上の問題が今すぐ懸念されることがないため、チュートリアルのこの時点ではそうしてもよいでしょう。
export JWT=`cat token1.jwt`
-
コンテナを再び実行します。今回はテストに成功し、課題1と同じメッセージが表示されました。
docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 200 Success apiKey1 apiclient-apiclient-1 exited with code 0
このため、少なくとも現時点ではベースイメージにはシークレットが含まれず、実行時に渡すことができるので、この方が安全です。ただし、まだ問題があります。
コンテナを検証する
-
API クライアントアプリケーションのコンテナ ID を取得するために、コンテナイメージについての情報を表示します。 (出力は、見やすいように、2行に渡っています。)
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTAINER ID NAMES IMAGE ... 6b20c75830df apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... CREATED STATUS ... 6 minutes ago Exited (0) 6 minutes ago ... About an hour ago Up About an hour
-
APIクライアントアプリケーションのコンテナを検査します。
<container_ID>
には、上の出力のCONTAINER
ID
フィールドから代入します(ここでは6b20c75830df
)。docker
inspect
コマンドは、現在実行しているかどうかには関係なく、起動した全てのコンテナを検査できます。そしてこれが問題なのですが、コンテナが実行されていない場合でも出力ではEnv
配列にJWTを公開し、コンテナ構成に安全に保存されません。docker inspect <container_ID> ... "Env": [ "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...", "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", "PYTHON_VERSION=3.11.2", "PYTHON_PIP_VERSION=22.3.1", "PYTHON_SETUPTOOLS_VERSION=65.5.1", "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..." ]
課題3: ローカルシークレットを使用する
ここまでで、シークレットのハードコーディングと環境変数の使用は、あなた(またはセキュリティチーム)が必要とするほど安全ではないことを学んだはずです。
セキュリティを向上させるために、ローカルの Docker シークレットを使用して機密情報を保存することができます。これも究極の方法ではありませんが、それがどのように機能するかを理解することは重要です。たとえ本番でDockerを使用しなくても、コンテナからシークレットを抽出するのをいかに難しくするか、ということが重要です。
Dockerでは、シークレットは/run/secrets/のファイルシステムマウントを介してコンテナに公開され、このパスに各シークレットの値が含まれる別々のファイルが配置されます。
この課題では、Docker Compose を使用してローカルに保存されたシークレットをコンテナに渡し、このメソッドを使用したときにシークレットがコンテナに表示されないことを確認します。
ローカルに保存されたシークレットをコンテナに渡す
-
まずはapiclientディレクトリに移動します。
cd ~/microservices-march/auth/apiclient
-
この課題のアプリケーション (コンテナ内のシークレットを使用するアプリケーション) を作業ディレクトリにコピーし、課題2のapp.pyファイルを上書きします。
cp ./app_versions/better_secrets.py ./app.py
-
/run/secrets/jotファイルからJWT値を読み取るPythonコードを見てみます。
cat app.py ... jotfile = "/run/secrets/jot" jwt = "" if os.path.isfile(jotfile): with open(jotfile) as jwtfile: for line in jwtfile: jwt = "Bearer " + line ...
それでは、このシークレットをどのように作成するのでしょうか?答えはdocker-compose.secrets.ymlファイルにあります。
-
Docker Composファイルをご覧ください。ここでは、シークレットファイルは
secrets
セクションで定義され、apiclient
サービスが参照しています。cat docker-compose.secrets.yml --- version: "3.9" secrets: jot: file: token1.jwt services: apiclient: build: . extra_hosts: - "host.docker.internal:host-gateway" secrets: - jot
シークレットがコンテナに表示されないことを確認する
-
アプリケーションを実行します。コンテナ内で JWT にアクセスできるようにしたため、認証はおなじみのメッセージで成功します。
docker compose -f docker-compose.secrets.yml up -build ... apiclient-apiclient-1 | 200 Success apiKey1 apiclient-apiclient-1 exited with code 0
-
コンテナイメージに関する情報を表示し、APIクライアントアプリケーションのコンテナIDをメモしてください。(出力例については、課題2の「コンテナを調べる」の手順1を参照してください)。
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
-
APIクライアントアプリケーションのコンテナを調べます。
<container_ID>
には、前の手順の出力にあるCONTAINER
ID
フィールドの値を代入してください。「コンテナを調べる」の手順2の出力とは異なり、Env
セクションの先頭にJWT=
行がありません。docker inspect <container_ID> "Env": [ "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", "PYTHON_VERSION=3.11.2", "PYTHON_PIP_VERSION=22.3.1", "PYTHON_SETUPTOOLS_VERSION=65.5.1", "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..." ]
ここまでは順調ですが、シークレットはコンテナファイルシステムの/run/secrets/jotにあります。課題1の「コンテナイメージからシークレットを取得する」と同じ方法を使用すれば、そこから抽出できるかもしれません。
-
(課題1 で作成した)extractディレクトリに移動し、コンテナをtarアーカイブにエクスポートします。
cd extract docker export -o api2.tar <container_ID>
-
tarファイルの中にあるシークレットを探します。
tar tvf api2.tar | grep jot -rwxr-xr-x 0 0 0 0 Mon DD hh:mm run/secrets/jot
なんと、JWTが含まれているファイルが表示されてしまいます。コンテナにシークレットを埋め込むことは「安全」だと言ったのに、課題1と同じような状況なのでしょうか?
-
見てみましょう – tarファイルからシークレットファイルを抽出し、その内容を確認します。
tar --extract --file=api2.tar run/secrets/jot cat run/secrets/jot
良い知らせです!
cat
コマンドの出力がないのは、コンテナファイルシステム内のrun/secrets/jotファイルが空であることを意味します。コンテナ内にシークレットアーティファクトがある場合でも、Docker は賢いので、コンテナ内に機密データを保存することはありません。
とはいえこのコンテナ構成は安全ですが、1つの欠点があります。それは、コンテナを実行する際に、ローカルファイルシステムにtoken1.jwtというファイルが存在するかどうかに依存していることです。このファイルをリネームするとコンテナの再起動が失敗します。(これを自分で試すには、token1.jwtをリネームして(削除はしないで!)、手順1のdocker
compose
コマンドを再度実行します)。
コンテナは、簡単には漏洩しないようにシークレットを使用しますが、ホスト上では、シークレットはまだ保護されていない状態です。シークレットを暗号化せずに、プレーンテキストファイルで保存されることは避けたいものです。シークレットを管理するツールを導入する時が来ました。
課題4: シークレットマネージャーを使用する
シークレットマネージャーは、ライフサイクル全体を通じてシークレットの管理、検索、ローテーションを支援します。シークレットマネージャーには多くの種類があり、どれも似たような目的を果たすことができます。
- シークレットの安全な保管
- アクセスの制御
- ランタイムへの配布
- シークレットのローテーションを有効化
シークレット管理のオプションは次のとおりです。
- クラウドプロバイダーはすべてシークレットサービスを持っています(たとえば、AWS Secrets Manager、Google Cloud PlatformのSecret Manager、Microsoft AzureのKey Vaultなど)
- Kubernetes にはSecretオブジェクトがあります
- Hashicorp Vaultは、クロスプラットフォームで人気のシークレットマネージャーです。
- OpenShiftにはシークレット管理サービスがあります。
- Docker Swarmにはシークレットサービスがあります。
わかりやすくするために、この課題では Docker Swarmを使用しますが、原理は多くのシークレットマネージャーで共通です。
この課題では、Dockerでシークレットを作成し、シークレットとAPIクライアントコードをコピーしてコンテナをデプロイし、シークレットを抽出できるかどうかを確認し、シークレットをローテーションします。
Dockerシークレットの設定
-
これまでと同様、まずはapiclientディレクトリに移動します。
cd ~/microservices-march/auth/apiclient
-
Docker Swarmを初期化します。
docker swarm init Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager. ...
-
シークレットを作成し、token1.jwtに格納します。
docker secret create jot ./token1.jwt qe26h73nhb35bak5fr5east27
-
シークレットに関する情報を表示します。シークレット値 (JWT) 自体が表示されていないことに注意してください。
docker secret inspect jot [ { "ID": "qe26h73nhb35bak5fr5east27", "Version": { "Index": 11 }, "CreatedAt": "YYYY-MM-DDThh:mm:ss.msZ", "UpdatedAt": "YYYY-MM-DDThh:mm:ss.msZ", "Spec": { "Name": "jot", "Labels": {} } } ]
Dockerシークレットを使用する
APIクライアントアプリケーションコードでDockerシークレットを使用することは、ローカルで作成したシークレットを使用することとまったく同じです。/run/secrets/のファイルシステムから読み取ることができます。必要なのは、Docker Compose YAMLファイルのsecret qualifierを変更することだけです。
-
Docker Compose YAMLファイルを見てみましょう。
external
フィールドにtrue
という値があり、Docker Swarmのシークレットを使用していることがわかります。cat docker-compose.secretmgr.yml --- version: "3.9" secrets: jot: external: true services: apiclient: build: . image: apiclient extra_hosts: - "host.docker.internal:host-gateway" secrets: - jot
つまりこのComposeファイルは、既存のAPIクライアントアプリケーションコードで動作することが期待できます。Docker Swarm (またはその他のコンテナオーケストレーションプラットフォーム) は多くの付加価値をもたらしますが、複雑さが増します。
docker
compose
は外部シークレットと連携しないので、Docker Swarmのコマンド、特にdocker
stack
deploy
を使用する必要があります。Docker Stackはコンソール出力を非表示にするため、出力をログに書き出してログを調べる必要があります。作業を簡単にするために、
while
True
ループを使用してコンテナを実行し続けます。 -
この課題のアプリケーション (シークレットマネージャーを使用するアプリケーション) を作業ディレクトリにコピーし、課題3のapp.pyファイルを上書きします。app.pyの内容を表示すると、課題3のコードとほぼ同じであることがわかります。唯一の違いは、
while
True
ループが追加されていることです。cp ./app_versions/best_secretmgr.py ./app.py cat ./app.py ... while True: time.sleep(5) try: with urllib.request.urlopen(req) as response: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message, file=sys.stderr) except urllib.error.URLError as e: print(str(e.code) + " " + e.msg, file=sys.stderr)
コンテナをデプロイし、ログを確認する
-
コンテナをビルドします(以前の課題では、Docker Composeがこれを処理しました)
docker build -t apiclient .
-
コンテナをデプロイします。
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack Creating network secretstack_default Creating service secretstack_apiclient
-
実行中のコンテナを一覧表示し、secretstack_apiclientのコンテナIDをメモします (出力は読みやすくするために複数行で表示されています)。
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTAINER ID ... 20d0c83a8b86 ... ad9bdc05b07c ... ... NAMES ... ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ... ... exciting_clarke ... ... IMAGE CREATED STATUS ... apiclient:latest 31 seconds ago Up 30 seconds ... apiserver 2 hours ago Up 2 hours
-
Dockerログファイルを表示します。
<container_ID>
には、前の手順の出力にあるCONTAINER
ID
フィールドの値(ここでは、20d0c83a8b86
)を代入してください。アプリケーションコードにwhile
True
ループを追加したため、ログファイルには一連の成功メッセージが表示されています。Ctrl+c
キーを押して、コマンドを終了します。docker logs -f <container_ID> 200 Success apiKey1 200 Success apiKey1 200 Success apiKey1 200 Success apiKey1 200 Success apiKey1 200 Success apiKey1 ... ^c
シークレットへのアクセスを試みる
この課題では、機密性の高い環境変数を設定していません(ただし、課題 2 の「コンテナを調べる」の手順2のように、docker
inspect
コマンドでいつでも確認できます)。
課題3から、/run/secrets/jotファイルが空であることもわかっており、以下を確認できます。
cd extract
docker export -o api3.tar
tar --extract --file=api3.tar run/secrets/jot
cat run/secrets/jot
成功しました!コンテナからシークレットを取得したり、Dockerシークレットから直接読み取ったりすることはできません。
シークレットのローテーション
もちろん適切な権限があれば、サービスを作成し、シークレットをログに読み込んだり、環境変数として設定したりするように構成できます。さらに、APIクライアントとサーバー間の通信が暗号化されていない(プレーンテキスト)ことにお気づきかもしれません。
そのため、ほとんどすべてのシークレット管理システムでシークレットの漏洩が発生する可能性があります。結果的に被害を受ける可能性を抑える方法の1つは、シークレットを定期的にローテーション(置換)することです。
Docker Swarmでは、シークレットを削除してから再作成することしかできません (Kubernetesではシークレットの動的更新が可能です)。また、実行中のサービスに付随するシークレットを削除することもできません。
-
実行中のサービスを一覧表示します。
docker service ls ID NAME MODE ... sl4mvv48vgjz secretstack_apiclient replicated ... ... REPLICAS IMAGE PORTS ... 1/1 apiclient:latest
-
secretstack_apiclientサービスを削除します。
docker service rm secretstack_apiclient
-
シークレットを削除し、新しいトークンで再作成します。
docker secret rm jot docker secret create jot ./token2.jwt
-
サービスを再作成します。
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
-
apiclient
のコンテナIDを調べます(サンプル出力については、「コンテナのデプロイとログの確認」の手順3を参照してください)docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
-
一連の成功メッセージを示すDockerログファイルを表示します。
<container_ID>
には、前の手順の出力にあるCONTAINER
ID
フィールドの値を代入してください。Ctrl+c
を押して、コマンドを終了します。docker logs -f <container_ID> 200 Success apiKey2 200 Success apiKey2 200 Success apiKey2 200 Success apiKey2 ... ^c
apiKey1
からapiKey2
への変化を見てください。シークレットをローテーションしました。
このチュートリアルでは、APIサーバーはまだ両方のJWTを受け入れていますが、本番環境では、JWTのクレームに特定の値を要求したり、JWTの有効期限をチェックすることで古いJWTを非推奨にすることができます。
また、シークレットを更新できるシークレット管理システムを使用している場合は、新しいシークレット値を取得するために、コードがシークレットを頻繁に読み直す必要があることに注意してください。
クリーンアップ
このチュートリアルで作成したオブジェクトをクリーンアップします。
-
secretstack_apiclientサービスを削除します。
docker service rm secretstack_apiclient
-
シークレットを削除します。
docker secret rm jot
-
swarmから抜けます(このチュートリアルのためだけにswarmを作成したと仮定)。
docker swarm leave --force
-
実行中のapiserverコンテナを強制終了します。
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
-
不要なコンテナを一覧表示して削除します。
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" docker rm <container_ID>
-
不要なコンテナイメージを一覧表示して削除します。
docker image list docker image rm <image_ID>
次のステップ
このブログを使用して、自分の環境でチュートリアルを実装したり、ブラウザーベースのラボで試したりすることができます (登録はこちら)。Kubernetesサービスの公開に関するトピックについてさらに学ぶには、ユニット2: マイクロサービスにおけるシークレット管理の基本の他のアクティビティに従ってください。
NGINX Plusを使用したプロダクショングレードのJWT認証の詳細については、ドキュメントを確認ください。ブログの「Authenticating API Clients with JWT and NGINX Plus」もご参照いただけます。